2

In iOS18, Apple added a new view contactAccessPicker to help select contacts that are available to an app. The SwiftUI example shows how it should be used:

Button {
        isPresented.toggle()
    } label: {
        Label("Add contacts", systemImage: "person.crop.circle.fill.badge.plus")
    }
    .contactAccessPicker(isPresented: $isPresented) { identifiers in
        lastAddedContacts = Set(identifiers)
        // Fetch all contacts the app has access to.
        fetchContacts()
    }

In my case, I have a UIKit app, so I need to use UIHostingController to show SwiftUI views. My 'button in this case is a UIBarButtonItem, and I would ideally like to show the contactAccessPicker presented on top of the view controller where that button is. But I'm not sure how I would handle this case where an "$isPresented" binding is needed to be shown, without having the problem of 'double presentation'. I.e. the simplest solution is to show a basic SwiftUI view (through UIHostingController) which then has this view modifier set on top of it. Here's an example:

struct CJContactAccessPickerHostedView: View {
@State private var isPickerPresented = true
var body: some View {
    Text("Hello, World!")
        .contactAccessPicker(isPresented: $isPickerPresented) { results in
            print("Access picker results = \(results)")
        }
}

}

Make it available to UIKit:

@objc static func makeContactsAccessPicker() -> UIViewController {        
    let hostcontroller = UIHostingController(rootView: CJContactAccessPickerHostedView())
    hostcontroller.title = "Select Contacts"
    return hostcontroller
}

Present in UIKit:

let rootViewController = SwiftUIViewFactory.makeContactsAccessPicker()
present(rootViewController, animated: true)

It works, but it first presents the SwiftUI view, and then it present the contactAccessPicker on top of that... causing a double presentation.

Is there a clean way to avoid that, and just show the contactAccessPicker view with the presentation binding through UIKit?

4
  • the only way is with an ObservableObject or an Observable, UIKit is the owner of the object and SwiftUI observes it. ObservableObject and @ObservedObject is the "better" solution because you can use Combine in UiKit if needed but Observable is the simplest. here Commented Aug 27, 2024 at 14:22
  • Try this Bro - @ZS Commented Sep 14, 2024 at 19:52
  • Did you find the solution for this issue? Running into same issue specially how to dismiss properly Commented Oct 10, 2024 at 21:52
  • In my case, it turned out that the UIViewController where I was planning to show this already had a SwiftUI view embedded in it, so I just used that to show the contactAccessPicker Commented Oct 14, 2024 at 16:56

1 Answer 1

1

Can you please check this

func presentContactAccessPicker() {
    if #available(iOS 18.0, *) {
        let pickerView = ContactAccessPickerHostingView(completionHandler: { [weak self] ids in
            guard let self else { return }
            DispatchQueue.main.async(execute: {
                guard let presentedController = self.presentedViewController, presentedController.isBeingDismissed == false else { return }
                self.dismiss(animated: true)
            })
         })
        let hostingController = UIHostingController(rootView: pickerView)
        hostingController.view.isHidden = true
        hostingController.modalPresentationStyle = .overCurrentContext
        self.present(hostingController, animated: false)
    }
}

@available(iOS 18.0, *)
struct ContactAccessPickerHostingView: View {
    @State var presented = true
    var handler: ([String]) -> ()
    
    init(completionHandler: @escaping ([String]) -> ()) {
        handler = completionHandler
    }
    
    var body: some View {
        Spacer()
            .contactAccessPicker(isPresented: $presented, completionHandler: handler)
            .onChange(of: presented) { newValue in
                if newValue == false {
                    handler([])
                }
            }
    }
}

Here's:


The ContactAccessPickerHostingView is a SwiftUI view that presents a contact picker and manages the user's selection. It utilizes a presented state variable to control the visibility of the picker. When displayed, users can select contacts, which are then passed to a handler—a closure that processes the selected contacts as an array of strings. The .onChange(of: presented) modifier listens for changes to this state variable. If the picker is dismissed without any selections (i.e., when presented becomes false), the handler is called with an empty array, ensuring proper management of cases with no selected contacts.

The presentContactAccessPicker() function facilitates showing the SwiftUI contact picker within a UIKit app. It checks for compatibility, creates the contact picker with a completion handler, and safely captures a weak reference to self to prevent memory leaks. The line guard let presentedController = self.presentedViewController, presentedController.isBeingDismissed == false else { return } ensures there is a visible view controller that is not being dismissed; if this condition is not met, the function exits early. The picker is embedded in a UIHostingController and presented over the current context without animation, providing a smooth user experience. This approach effectively integrates SwiftUI and UIKit for managing contact selection in an iOS app.

Overall, the .onChange(of: presented) modifier is crucial for handling the dismissal of the picker, especially when the user swipes down to close it. When presented changes to false, the associated closure is triggered, executing the handler with an empty array. This enables the app to respond appropriately to the visibility changes of the picker, ensuring a seamless user experience.

Sign up to request clarification or add additional context in comments.

1 Comment

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.