Duck
Duck

Reputation: 35993

Popover not showing, no matter what

I have this code:

import SwiftUI

struct ContentView: View {
  @State var show = false
  
  var tools = [Tool("document.on.document"), Tool("document.on.clipboard")]
  
  var body: some View {
    HStack{
      HStack {
        ForEach(tools,  id:\.id) { tool in
          Button(action: {
            print("toggle")
            show.toggle()
          }, label: {
            Image(systemName:tool.name)
              .imageScale(.large)
              .foregroundStyle(.black.gradient)
              .font(.system(size: 30))
          })
          .contentShape(Rectangle())
          .popover(isPresented: $show, arrowEdge: .top) {
            Color.red
              .frame(width:400, height:400)
              .onAppear{
                print("popover show")
              }
          }
        }

        
      }

      .padding()
    }
    .background(
      RoundedRectangle(cornerSize: CGSize(width: 50,height: 50))
        .fill(.red.opacity(0.5).gradient))
    
    .padding()
    
    
  }
}

#Preview {
  ContentView()
}

struct Tool: Identifiable, Equatable {
  let id = UUID()
  let name:String
  
  init(_ name: String) {
    self.name = name
  }
}

the popover does not show. I did everything.

Why is that?

Upvotes: 2

Views: 558

Answers (2)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119676

A single boolean can not control multiple presentations.

You can make a binding for each tool like:

ForEach(tools,  id:\.id) { tool in
    let binding = Binding<Bool>(
        get: { selectedTool == tool }, // 👈 Only if the current tool is selected
        set: { selectedTool = $0 ? tool : nil } // 👈 Toggles only with the current tool
    )

    Button(action: {
        print("toggle")
        selectedTool = tool
    }, label: {
        Image(systemName: tool.name)
            .imageScale(.large)
            .foregroundStyle(.black.gradient)
            .font(.system(size: 30))
    })
    .contentShape(Rectangle())
    .popover(isPresented: binding, arrowEdge: .top) {  // 👈 Use the binding corresponding to the current tool here
        Color.red
            .frame(width:400, height:400)
            .onAppear{
                print("popover show")
            }
    }
}

Upvotes: 0

Sweeper
Sweeper

Reputation: 272715

You are using a single show state to control all of the popovers! Multiple popovers cannot be shown at the same time, so noting gets shown.

It is technically possible to change the type of show to [Bool] and pass $show[0], $show[1] etc to isPresented, but it would be quite inconvenient to find the index of the tool and things like that.

If the parent view doesn't need to know exactly which popovers are shown, you should extract the button into a separate view, and put the show state there.

struct ToolButton: View {
    // move the @State here
    @State private var show = false
    let tool: Tool

    // pass anything else you need here...
    
    var body: some View {
        Button(action: {
            print("toggle")
            show.toggle()
        }, label: {
            Image(systemName: tool.name)
                .imageScale(.large)
                .foregroundStyle(.black.gradient)
                .font(.system(size: 30))
        })
        .contentShape(Rectangle())
        .popover(isPresented: $show, arrowEdge: .top) {
            Color.red
                .frame(width:400, height:400)
                .presentationCompactAdaptation(.popover) // if you also want a popover on iPhones
                .onAppear{
                    print("popover show")
                }
        }
    }
}

Since the @State is in ToolButton, there will be as many states as there are ToolButton, which means one state to control each popover.

ForEach(tools) { tool in
    ToolButton(tool: tool)
}

Upvotes: 4

Related Questions