swiftswiftuiuikituiviewcontrollerrepresentable

UIViewControllerRepresentable doesn't update currently only in iOS 14


I created UIViewControllerRepresentable to present popover.

I did test it on Xcode 13 and iOS 15 it working great as video on this link

But it doesn't work as expected on Xcode 12.5.1 and iOS 14.8 as video on this link

I don't know why it doesn't work on iOS 14.8 , is there other solution to make it work on both iOS versions ?

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 cocoaPopover<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
        host.rootView = content()
        
        if host.viewIfLoaded?.window == nil && 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 if self.isPresented == false {
            host.dismiss(animated: false, 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
        }
    }
}

Solution

  • Your issue has nothing to do with the Representable. It seems there may be a bug in iOS14 and the way it handles buttons on a Toolbar. When you place a button inside a Toolbar, its geometry behavior is a little strange (on iOS14). Run the following code and check the results on both iOS versions.

    struct ContentView: View {
        @State var show = false
        
        var body: some View {
            NavigationView {
                VStack {
                    Text("Hello, world!")
                }
                .toolbar {
                    Button("Show") {
                        show = true
                    }
                    .border(Color.green)
                }
            }
        }
    }
    

    On iOS 15, it will look like this:

    enter image description here

    but on iOS 14, it looks like this:

    enter image description here

    So what happened to the green border? Anything placed in a border, overlay or background gets drawn somewhere else (and for what I've seen so far, out of the screen).

    Note that this problem only occurs for buttons inside a toolbar.

    This bad geometry has a direct effect on your popover. So if we find a way to fix that geometry, the problem for your popover will go away too. I can think of two workarounds:

    1. Replace your button with a tappable text:
    Text("Show").onTapGesture { ... }
    

    This works, but if you want to emulate the look of a real button, you'll need to put some more work. Fortunately, there's workaround number 2.

    1. Use a transparent Text as your toolbar item, and place the button over it:
    struct ContentViewx: View {
        @State var show = false
        
        var body: some View {
            NavigationView {
                VStack {
                    Text("Hello, world!")
                }
                .toolbar {
                    Text("Show")
                        .opacity(0)
                        .overlay(
                            Button("Show") {
                                show = true
                            }
                        )
                        .cocoaPopover(isPresented: $show) {
                            print("dismiss!")
                        } content: {
                            Text("Hello, this is the popover!")
                        }
                }
            }
        }
    }