ContactPicker in SwiftUI
Allowing users to pick selected contacts in your SwiftUI app
Over the years, a lot of apps have come to rely on system contacts for enabling certain sharing or social features.
There are two ways to get access to the contacts on the system depending on your use case:
1. Getting access to all system contacts - Intrusive
In this case, the app will need to present a prompt requesting user permission to access contacts. A lot of privacy conscious users are uncomfortable with this option because once they have enabled this permission, they have no idea what the app is doing with their contacts. So unless it is absolutely necessary or a core paradigm of your app, it is best to not request full access to contacts.
2. Getting access to specific contacts - Privacy Friendly
In case you need to import or read details for a specific contact (or a few contacts), Apple offers a privacy-preserving way to get access to these contacts. Using the built-in CNContactPicker, the app can present a system sheet where users can select one or more contacts that they want the app to access. In this case, the app need not even ask for contact read permissions because this is a user intended action and the app does not have access to any of the other contacts.
In this article, we go over how to use this Contact Picker in SwiftUI.
Implementation
While you’d think this would be simple to do in SwiftUI, it unfortunately is one of those components that is still exclusive to UIKit.
The UIKit contacts sheet is called CNContactPickerViewController
You can set this up with a UIViewControllerRepresentable
to present it as a sheet in your SwiftUI app.
import SwiftUI
import ContactsUI
public struct ContactPicker: UIViewControllerRepresentable {
@Binding var isPresented: Bool
public func makeUIViewController(context: Context) -> some UIViewController {
let navController = UINavigationController()
let pickerVC = CNContactPickerViewController()
pickerVC.delegate = context.coordinator
navController.pushViewController(pickerVC, animated: false)
navController.isNavigationBarHidden = true
return navController
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
At this point you are probably wondering why we can’t directly return the pickerVC
itself. Why do we need a navigation controller wrapping the picker? Turns out this is a weird quirk of CNContactPickerViewController. When trying to present directly, the picker would work like an empty sheet with no way to present your contacts. So, the only way to actually show meaningful information is to wrap it around a UINavigationController (or for that matter, any other view controller - it just cannot be the CNContactPickerViewController directly).
The Delegate
The picker view needs a delegate that conforms to the CNContactPickerDelegate
protocol. This protocol has multiple functions (you can see the whole list here - https://developer.apple.com/documentation/contactsui/cncontactpickerdelegate) but for the purpose of this article, we will assume single contact selection and cancellation only.
public func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
public class Coordinator: NSObject, CNContactPickerDelegate {
var parent: ContactPicker
init(parent: ContactPicker) {
self.parent = parent
}
public func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
}
public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
print("Your chosen contact is : \(contact.givenName)")
// You can use your contact here or pass it on to your parent component
}
}
In case you want to enable selection of multiple contacts, you can remove the contactPicker:didSelectContact:
method and replace it with contactPicker:didSelectContacts:
, this method returns an array of contacts that you can then process in your app.
You can check out the full component here -
import SwiftUI | |
import ContactsUI | |
public struct ContactPicker: UIViewControllerRepresentable { | |
@Binding var isPresented: Bool | |
var onSelect: (CNContact) -> Void | |
public func makeUIViewController(context: Context) -> some UIViewController { | |
let navController = UINavigationController() | |
let pickerVC = CNContactPickerViewController() | |
pickerVC.delegate = context.coordinator | |
navController.pushViewController(pickerVC, animated: false) | |
navController.isNavigationBarHidden = true | |
return navController | |
} | |
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { | |
} | |
public func makeCoordinator() -> Coordinator { | |
return Coordinator(parent: self) | |
} | |
public class Coordinator: NSObject, CNContactPickerDelegate { | |
var parent: ContactPicker | |
init(parent: ContactPicker) { | |
self.parent = parent | |
} | |
public func contactPickerDidCancel(_ picker: CNContactPickerViewController) { | |
} | |
public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { | |
parent.onSelect(contact) | |
} | |
} | |
} | |
struct ContactPickerViewModifier: ViewModifier { | |
@Binding var isPresented: Bool | |
var onDismiss: (() -> Void)? | |
var onSelect: ((CNContact) -> Void) | |
func body(content: Content) -> some View { | |
content | |
.sheet(isPresented: $isPresented, onDismiss: onDismiss) { | |
ContactPicker(isPresented: $isPresented, onSelect: onSelect) | |
} | |
} | |
} | |
public extension View { | |
func contactPicker(isPresented: Binding<Bool>, onDismiss: (() -> Void)?, onSelect: @escaping ((CNContact) -> Void)) -> some View { | |
modifier(ContactPickerViewModifier(isPresented: isPresented, onDismiss: onDismiss, onSelect: onSelect)) | |
} | |
} |