Attaching .popover to a ForEach or Section within a List creates multiple popovers

Viewed 773

I have a List with multiple Section and each Section has different type of data. For each section I want clicking on an item to present a popover.

Problem is that if I attach the .popover to the Section or ForEach then the .popover seems to be applied to every entry in the list. So the popover gets created for each item even when just one is clicked.

Example code is below. I cannot attach the .popover to the List because, in my case, there are 2 different styles of .popover and each view can only have a single .popover attached to it.

struct Item: Identifiable {
    var id = UUID()
    var title: String
}

var items: [Item] = [
    Item(title: "Item 1"),
    Item(title: "Item 2"),
    Item(title: "Item 3"),
]

struct PopoverView: View {
    @State var item: Item
    
    var body: some View {
        print("new PopoverView")
        return Text("View for \(item.title)")
    }
}

struct ContentView: View {
    @State var currentItem: Item?
    
    var body: some View {
        List {
            Section(header: Text("Items")) {
                ForEach(items) { item in
                    Button(action: { currentItem = item }) {
                        Text("\(item.title)")
                    }
                }
            }
        }
    }
}

The current best solution I have come up with is to attach the popover to each Button and then only allow one popover based on currentItem,

                    Button(action: { currentItem = item }) {
                        Text("\(item.title)")
                    }
                    .popover(isPresented: .init(get: { currentItem == item },
                                                set: { $0 ? (currentItem = item) : (currentItem = nil) })) {
                        PopoverView(item: item)
                    }

Any better way to do this?

Bonus points to solve this: When I used my hack, the drag down motion seems to glitch and the view appears from the top again. Not sure what the deal with that is.

3 Answers

You can always create a separate view for your item.

struct MyGreatItemView: View {
    @State var isPresented = false
    var item: Item
   
    var body: some View {

          Button(action: { isPresented = true }) {
                        Text("\(item.title)")
                    }
           .popover(isPresented: $isPresented) {
                        PopoverView(item: item)
                    }

    }
}

And implement it to ContentView:

struct ContentView: View {  
    var body: some View {
        List {
            Section(header: Text("Items")) {
                ForEach(items) { item in
                   MyGreatItemView(item: item)
                }
            }
        }
    }
}

Trying to reach component like sheet or popover in ForEach causes problems. I've also faced the glitch you mentioned, but below (with sheet) works as expected;

List {
    Section(header: Text("Items")) {
        ForEach(items) { item in
            Button(action: { currentItem = item }) {
                Text("\(item.title)")
            }
        }
    }
}
.sheet(item: $currentItem, content: PopoverView.init)

Here's a late suggestion, I used a ViewModifier to hold the show Popover state on each view, the modifier also builds the Popover menu and also handles the presented sheet initiated from the popover menu. (Here's some code...)


struct Item: Identifiable {
  var id = UUID()
  var title: String
}

var items: [Item] = [
  Item(title: "Item 1"),
  Item(title: "Item 2"),
  Item(title: "Item 3"),
]

struct ContentView: View {
  
  var body: some View {
      List {
          Section(header: Text("Items")) {
              ForEach(items) { item in
                  Text("\(item.title)").popoverWithSheet(item: item)
              }
          }
      }
  }
}

struct SheetFromPopover: View {
  @State var item: Item
  
  var body: some View {
      print("new Sheet from Popover")
      return Text("Sheet for \(item.title)")
  }
}

struct PopoverModifierForView : ViewModifier {
  
  @State var showSheet : Bool = false
  @State var showPopover : Bool = false
  
  var item : Item
  var tap: some Gesture {
      TapGesture(count: 1)
          .onEnded { _ in self.showPopover = true }
  }
  
  func body(content: Content) -> some View {
      content
      .popover(isPresented: $showPopover,
                      attachmentAnchor: .point(.bottom),
                      arrowEdge: .bottom) {
          self.createPopover()
      }
      .sheet(isPresented: self.$showSheet) {
          SheetFromPopover(item: item)
      }
      .gesture(tap)
      
  }
  
  func createPopover() -> some View {
      VStack {
          Button(action: {
              self.showPopover = false
              self.showSheet = true
          }) {
              Text("Show Sheet...")
          }.padding()

          
          Button(action: {
              print("Something Else..")
          }) {
              Text("Something Else")
          }.padding()
      }
  }
}

extension View {
  func popoverWithSheet(item: Item) -> some View {
      modifier(PopoverModifierForView(item: item))
  }
}


struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
      ContentView()
  }
}
Related