Contact Management: Working with the Contact Picker View Controller
Learn how to use the default controls for handling contacts provided by Apple in your SwiftUI app.
To allow the user to select a contact already existing in the system address book, it is possible to use the CNContactPickerViewController component, which is available in the ContactsUI framework.
Although it is a UIKit component, it can be integrated with SwiftUI.
First of all, to access contacts through an app, you need to add the Privacy - Contacts Usage Description key to your Info.plist file. Also, keep in mind that for your app to access contacts, it must be tested in the Xcode Simulator or on a physical device.
The initial structure should focus on adapting a UIKit component for use in SwiftUI. The protocol that will allow this is UIViewControllerRepresentable.
struct ContactPickerView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
// TODO
}
func makeUIViewController(context: Context) -> CNContactPickerViewController {
// TODO
}
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {
// TODO
}
}
To do this, you need to create a few functions. The first is makeUIViewController() to return a CNContactPickerViewController, which is the native iOS contact picker. Within this function, a delegate assigned to context.coordinator to capture the selected contact.
func makeUIViewController(context: Context) -> CNContactPickerViewController {
let picker = CNContactPickerViewController()
picker.delegate = context.coordinator
return picker
}
The method updateUIViewController(_:context:) is used to update the UI when something in SwiftUI changes; however, it is empty in this case because there is nothing to dynamically update in the picker.
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {
// Nothing to do here!
}
The method makeCoordinator() creates an instance of a Coordinator object, which will be the delegate of CNContactPickerViewController. The Coordinator class will be declared inside our view and conform to the CNContactPickerDelegate protocol.
In our example, we also create a reference to the view model object that will handle contact information.
struct ContactPickerView: UIViewControllerRepresentable {
@ObservedObject var viewModel: ViewModel
func makeCoordinator() -> Coordinator {
Coordinator(viewModel: viewModel)
}
class Coordinator: NSObject, CNContactPickerDelegate {
var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
}
}
Inside the Coordinator class, implement the method contactPicker(_:didSelect:), part of the CNContactPickerDelegate protocol, so that when the user selects a contact, givenName and familyName are assigned to selectedName, which can be handled in a separate ViewModel object, for example.
class Coordinator: NSObject, CNContactPickerDelegate {
var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
viewModel.selectedName = "\(contact.givenName) \(contact.familyName)"
if let number = contact.phoneNumbers.first?.value.stringValue {
viewModel.selectedPhone = number
} else {
viewModel.selectedPhone = "No phone available"
}
}
}
In summary:
CNContactPickerViewControllercreates a native contact selection interface.Coordinatorimplements thedelegate, bridging UIKit to SwiftUI.CNContactPickerDelegatenotifies when a contact is selected.
Here is an example of the complete implementation and usage of the CNContactPickerViewController within a SwiftUI app.
import Foundation
import ContactsUI
import SwiftUI
struct ContactPickerView: UIViewControllerRepresentable {
@ObservedObject var viewModel: ViewModel
func makeCoordinator() -> Coordinator {
Coordinator(viewModel: viewModel)
}
func makeUIViewController(context: Context) -> CNContactPickerViewController {
let picker = CNContactPickerViewController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) { }
class Coordinator: NSObject, CNContactPickerDelegate {
var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
viewModel.selectedName = "\(contact.givenName) \(contact.familyName)"
if let number = contact.phoneNumbers.first?.value.stringValue {
viewModel.selectedPhone = number
} else {
viewModel.selectedPhone = "No phone available"
}
}
}
}
import Foundation
import Contacts
class ViewModel: NSObject, ObservableObject {
@Published var selectedName: String = ""
@Published var selectedPhone: String = ""
private let contactStore = CNContactStore()
func requestAccessIfNeeded(completion: @escaping (Bool) -> Void) {
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .authorized:
completion(true)
case .notDetermined:
contactStore.requestAccess(for: .contacts) { granted, _ in
DispatchQueue.main.async {
completion(granted)
}
}
default:
completion(false)
}
}
}
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
@State private var showPicker = false
@State private var permissionDenied = false
var body: some View {
NavigationStack {
Form {
Button("Select Contact") {
viewModel.requestAccessIfNeeded { granted in
if granted {
showPicker = true
} else {
permissionDenied = true
}
}
}
if !viewModel.selectedName.isEmpty { // by design only
Section(header: Text("Selected Contact")) {
Text("Name: \(viewModel.selectedName)")
Text("Phone: \(viewModel.selectedPhone)")
}
}
}
.navigationTitle("Contact Picker")
.sheet(isPresented: $showPicker) {
ContactPickerView(viewModel: viewModel)
}
.alert("Access Denied", isPresented: $permissionDenied) {
Button("OK", role: .cancel) { }
} message: {
Text("Please allow access to contacts in Settings.")
}
}
}
}