Reputation: 11427
(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
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