Contact Management: Working with the Contact Picker View Controller

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:


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.")
            }
        }
    }
}