Andrew
Andrew

Reputation: 11427

SwiftUI: is it possible to use ForEach + ContextMenu + if statement?

(tried on macOS only, but I believe there is the same behaviour on iOS)

macOS 10.15.2 (19C57); Xcode: 11.3 (11C29)

(question was updated because of I have found new relations in the issue)

I have the following code:

List(model.filteredStatus) { status in
    StatusViewRow(status: status, model: self.model)
        .contextMenu{
            Text("menu 1")
            Text("menu 2")
        }
}

It's works perfectly.

But in case of I want to use ForEach instead ( no matter why):

ForEach(model.filteredStatus) { status in
    StatusViewRow(status: status, model: self.model)
        .contextMenu{
            Text("menu 1")
            Text("menu 2")
        }
}

Context menu doesn't appear.

I have found the reason. Reason was in incorrect work of ForEach + contextMenu + if statement combination!

ContextMenu doesn't work exactly on part of row inside of the if statement. Works well on the ful row with List and works only on the toogle row part when I'm using ForEach.

Can somebody explain why so?

I have an if statement inside of StatusViewRow contextMenu doesn't appear on this part:

struct StatusViewRow : View {
    @ObservedObject var status : FileStatusViewModel
    @ObservedObject var model : StatusViewModel

    var body: some View {
        HStack {
            TaoToggle(state: status.stageState) { self.status.toggleStageState() }
            // you can replace to just a toogle

// ISSUE START HERE 
            if(model.fileNamesOnly) {
                StyledText(verbatim: status.fileName )
                    .style(.highlight(), ranges: { [$0.lowercased().range(of: model.filterStr.lowercased() )]})
            } 
            else {
                StyledText(verbatim: status.path )
                    .style(.foregroundColor(.gray), ranges: { [$0.range(of: status.fileDir) ] } )
                    .style(.bold(),                 ranges: { [$0.range(of: status.fileName) ] })
                    .style(.highlight(),            ranges: { [$0.lowercased().range(of: model.filterStr.lowercased() )]})
            }           
// ISSUE END HERE
        }
    }
}

Upvotes: 7

Views: 3222

Answers (1)

gotnull
gotnull

Reputation: 27224

You need to wrap ForEach inside a List or relevant.

Any view that contains Text("...") needs to be either inside a VStack, HStack, ZStack, etc or a List. It won't render otherwise.

When you create a SwiftUI view, the protocol defines that we need to return “some View”. The word “some” means that we are dealing with an opaque result type. This is a new feature added in Swift 5.1. An opaque result type means that we must return one, and only one, of the specified type, in this case something that conforms to the “View” protocol.

ForEach conforms to Hashable:

struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable

...whereas stacks (VStack in this case) conforms to View:

@frozen struct VStack<Content> where Content : View

...and List also conforms to View:

struct List<SelectionValue, Content> where SelectionValue : Hashable, Content : View

So you need something like this:

ForEach (model.filteredStatus) { status in
    VStack { // stack View
        StatusViewRow(status: status, model: self.model)
            .contextMenu {
                Text("Menu 1")
                Text("Menu 2")

            }
    }
}

OR:

ForEach (model.filteredStatus) { status in
    StatusViewRow(status: status, model: self.model)
        .contextMenu {
            VStack { // stack View
                Text("Menu 1")
                Text("Menu 2")
            }
        }

}

OR:

List { // List works because it conforms to View
    ForEach (model.filteredStatus) { status in
        StatusViewRow(status: status, model: self.model)
            .contextMenu {
                Text("Menu 1")
                Text("Menu 2")
            }

    }
}

This article does a good job of explaining why it's necessary.

Upvotes: 2

Related Questions