swiftswiftuipopoveruipopovercontrolleruiviewcontrollerrepresentable

Popover crash when update value using UIViewControllerRepresentable


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()
                    }
                }
}
}

}


Solution

  • 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.