0

I want to make popover on iPhone , I notice when using .popover in iPhone it will always show as sheet , but on iPad it will show as popover

so I decide to use UIKit version everything is working fine until I tap on Button to update the view

it will crash with this error

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally a view controller <_TtGC7SwiftUI19UIHostingControllerGVS_6HStackGVS_9TupleViewTGVS_6ButtonVS_4Text_S4_GS3_S4______: 0x7fd47b424df0> that is already being presented by <UIViewController: 0x7fd47b426200>.

My code :

struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
    @Binding var isPresented: Bool
    let onDismiss: (() -> Void)?
    let content: () -> PopoverContent

    func body(content: Content) -> some View {
        content
            .background(
                Popover(
                    isPresented: self.$isPresented,
                    onDismiss: self.onDismiss,
                    content: self.content
                )
            )
    }
}

extension View {
    func popover<Content>(
        isPresented: Binding<Bool>,
        onDismiss: (() -> Void)? = nil,
        content: @escaping () -> Content
    ) -> some View where Content: View {
        ModifiedContent(
            content: self,
            modifier: PopoverViewModifier(
                isPresented: isPresented,
                onDismiss: onDismiss,
                content: content
            )
        )
    }
}

struct Popover<Content: View> : UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    let onDismiss: (() -> Void)?
    @ViewBuilder let content: () -> Content

    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self, content: self.content())
    }

    func makeUIViewController(context: Context) -> UIViewController {
        return UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        let host = context.coordinator.host
        if self.isPresented {
            host.preferredContentSize = host.sizeThatFits(in: CGSize(width: Int.max , height: Int.max))
            host.modalPresentationStyle = UIModalPresentationStyle.popover
            host.popoverPresentationController?.delegate = context.coordinator
            host.popoverPresentationController?.sourceView = uiViewController.view
            host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
            uiViewController.present(host, animated: true, completion: nil)
        }
        else {
            host.dismiss(animated: true, completion: nil)
        }
    }

    class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
        let host: UIHostingController<Content>
        private let parent: Popover

        init(parent: Popover, content: Content) {
            self.parent = parent
            self.host = UIHostingController(rootView: content)
        }

        func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
            self.parent.isPresented = false
            if let onDismiss = self.parent.onDismiss {
                onDismiss()
            }
        }

        func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
            return .none
        }
    }
}

How I use it :

struct ContentView: View {
@State var openChangeFont = false
@State var currentFontSize = 0

var body: some View {
    
    NavigationView {

        Text("Test")
          
  
        .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    Text("Popover")
                        .popover(isPresented: $openChangeFont, content: {
                            HStack {
                                
                                Button(action: {
                                    DispatchQueue.main.async {
                                        currentFontSize += 2
                                    }
                                }, label: {
                                    Text("Increase")
                                })
                                
                                Text("\(currentFontSize)")
                                
                                Button(action: {
                                    currentFontSize -= 2

                                }, label: {
                                    Text("Decrease")
                                })

                            }
                    })
                        .onTapGesture {
                            openChangeFont.toggle()
                    }
                }
}
}

}

Basel
  • 550
  • 8
  • 21

1 Answers1

1

Put a breakpoint in Popover.updateUIViewController and I think you’ll catch the problem. updateUIViewController is called every time the SwiftUI view is updated, which means it may be called when isPresented is true and you the popover is already being presented.

If that’s the issue, then you need to track whether you’re already presenting the popover or not. You’re already implementing UIPopoverPresentationControllerDelegate so you can use that.

Adam
  • 4,405
  • 16
  • 23
  • I tried to change the logic inside `updateUIViewController` by adding `host.viewIfLoaded?.window == nil && self.isPresented`it solve the crash , but every time the view update it will dismiss – Basel Oct 11 '21 at 13:08
  • 1
    If you added that to your `if-else`, make sure you aren’t falling through to the `else` that dismisses the popover. You basically want two `if` statements: `if host.viewIfLoaded?.window == nil && self.isPresented` then show popover; `if self.isPresented == false` then hide popover. – Adam Oct 11 '21 at 14:31
  • currently it won't dismiss , and it won't crash , but it won't update should I pass currentFontSize to UIViewControllerRepresentable or is there other way ? – Basel Oct 11 '21 at 19:27
  • 1
    I’m not sure what you’re trying to update, but check out the `Context` that’s passed in to updateUIViewController — it contains all of the SwiftUI environment values, such as font and accent color, so whatever you need is likely in there. If you’re still stuck, I’d suggest posting a new question with your current code. – Adam Oct 11 '21 at 19:51
  • Thank you very much ,I figure it out , I add `host.rootView = content()` after `let host = context.coordinator.host` , now everything working fine – Basel Oct 11 '21 at 20:11