Contact Provider Extension
The ContactProvider framework lets your app create (business) contacts on the user's device.
It is the prefered way to add contacts to the user's address book without interferring with their (personal) address book.
In this post we will create a simple app that uses the ContactProvider
framework to add a business contact to the user's address book.
How are we going to setup our application?
We want these features:
- Adding a contact.
- Reset the contacts database.
- Sync to the contact provider extension (yes for this demo we'll do it manually).
We need a way to easily add and fetch contacts from our app's "database".
We will create an observable object that will hold the contacts.
Required actions for the app and extension
In our app we need to do the following actions:
-
We need to tell the system that we are able to provide contacts.
let manager = try ContactProviderManager() try await manager.enable()
-
We need to synchronize the contacts with the system.
let manager = try ContactProviderManager() try await manager.signalEnumerator()
In our extension we need to conform to the ContactItemEnumerator
protocol.
- enumeratecontent(in:for:) for syncing the initial contacts.
- enumeratechanges(startingat:for:) for syncing the changes.
Building the app
Add the Contact Provider Extension
in your app.
To enable ContactProvider
in your app, you need to add the Contact Provider Extension
to your app.
Setup an App Group
To enable communication between your app and the Contact Provider Extension
, you need to setup an App Group.
For this example we will use the group.nl.wesleydegroot.contactproviderdemo
App Group.
Building the main application
import ContactProvider
struct ContentView: View {
/// The database that holds the contacts.
@ObservedObject
var database = ContactItemDatabase()
@State var firstName: String = ""
@State var lastName: String = ""
@State var email: String = ""
@State var phoneNumber: String = ""
/// The contact provider manager.
let manager = try! ContactProviderManager()
var body: some View {
VStack {
Button("Send to contact provider") {
Task {
// Synchronise contacts
await synchroniseContacts()
}
}
.buttonStyle(.borderedProminent)
Button("Reset Database") {
Task {
// Reset the database
database.reset()
// Reset all previously known contacts.
try? await manager.reset()
}
}
.buttonStyle(.borderedProminent)
List {
ForEach(database.contactItemsJSON, id: \.firstName) { item in
Text("\(item.firstName) \(item.lastName)")
}
}
GroupBox {
TextField("First name", text: $firstName)
TextField("Last name", text: $lastName)
TextField("Email", text: $email)
TextField("Phone number", text: $phoneNumber)
Button("Add") {
saveToDatabase()
}
}
.task {
randomContact()
}
.background(.secondary)
}
.padding()
.task {
do {
// May prompt the person to enable the default domain.
try await manager.enable()
} catch {
// Handle the error.
}
}
}
func saveToDatabase() {
database.save(
.init(
firstName: firstName,
lastName: lastName,
phoneNumber: phoneNumber == "" ? nil : phoneNumber,
emailAddress: email == "" ? nil : email
)
)
Task {
randomContact()
}
}
func randomContact() {
firstName = ["John", "Jane", "Wesley", "Alice", "Bob"].randomElement()!
lastName = ["Doe", "Smith", "de Groot", "Johnson", "Williams"].randomElement()!
email = "\(firstName.lowercased()).\(lastName.lowercased())@example.com"
phoneNumber = "+31612345678"
}
func synchroniseContacts() async {
do {
try await manager.signalEnumerator()
} catch {
// Handle the error.
print(error)
}
}
}
Building the "middleware" ContactItemDatabase
import Foundation
import Contacts
import ContactProvider
class ContactItemDatabase: ObservableObject {
// The items that will be synchronised to the Contact Provider extension.
@Published public var contactItems: [ContactItem] = []
// The items that will be saved to the database.
@Published public var contactItemsJSON: [ContactItemJSON] = []
// The url of our shared database. (App Group)
private let fileURL = FileManager.default.containerURL(
// App Group identifier.
forSecurityApplicationGroupIdentifier: "group.nl.wesleydegroot.contactproviderdemo"
)?.appendingPathComponent("ContactItems").appendingPathExtension("json")
// The JSON struct that will be saved to the database.
struct ContactItemJSON: Codable, Hashable {
/// The identifier of the contact.
var identifier: String = UUID().uuidString
/// The first name of the contact. (givenName)
var firstName: String
/// The last name of the contact.
var lastName: String
/// The phone number of the contact.
var phoneNumber: String?
/// The email address of the contact.
var emailAddress: String?
}
init() {
guard let fileURL else {
print("Could not find file URL; is the App Group identifier correct?")
return
}
// Load the contents of our json (database) file.
if let data = try? Data(contentsOf: fileURL) {
let decoder = JSONDecoder()
// Decode the JSON data from our ContactItemJSON struct.
if let decoded = try? decoder.decode([ContactItemJSON].self, from: data) {
// Save the decoded data to our contactItemsJSON array.
// And remove any duplicates Array(Set(...)).
contactItemsJSON = Array(Set(decoded))
// Walk trough our contacts
for item in contactItemsJSON {
// Create a new contact.
let contact: CNMutableContact = .init()
// Set the first name of the contact.
contact.givenName = item.firstName
// Set the last name of the contact.
contact.familyName = item.lastName
// If the contact has a phone number, add it to the contact.
if let phoneNumber = item.phoneNumber {
contact.phoneNumbers = [
.init(
label: "Mobile",
value: CNPhoneNumber(stringValue: phoneNumber)
)
]
}
// If the contact has an email address, add it to the contact.
if let emailAddress = item.emailAddress {
contact.emailAddresses = [
.init(
label: "Email",
value: emailAddress as NSString
)
]
}
// Create a new contact item identifier, from the contact identifier.
let identifier: ContactItem.Identifier = .init(item.identifier)
// Add the contact to the contactItems array.
contactItems.append(
.contact(contact, identifier)
)
}
}
}
}
/// Reset the database.
func reset() {
// Remove all `ContactItem` items.
contactItems.removeAll()
// Remove all `ContactItemJSON` items.
contactItemsJSON.removeAll()
// Save the empty array to the database.
try? JSONEncoder().encode(contactItemsJSON).write(to: fileURL!)
}
func save(_ contactItem: ContactItemJSON) {
// Append the new contact to the contactItemsJSON array.
contactItemsJSON.append(contactItem)
// Save the new contact to the database.
try? JSONEncoder().encode(contactItemsJSON).write(to: fileURL!)
}
}
Building the ContactProvider Extension
import Foundation
import ContactProvider
import Contacts
import OSLog
@main
class Provider: ContactProviderExtension {
private let rootContainerEnumerator: ProviderRootContainerEnumerator
required init() {
rootContainerEnumerator = ProviderRootContainerEnumerator()
}
func configure(for domain: ContactProviderDomain) {
rootContainerEnumerator.configure(for: domain)
}
func enumerator(for collection: ContactItem.Identifier) -> ContactItemEnumerator {
return rootContainerEnumerator
}
func invalidate() async throws {
// TODO: Stop any enumeration and cleanup as the extension will be terminated.
}
}
class ProviderRootContainerEnumerator: ContactItemEnumerator {
/// The database that holds the contacts.
private let database = ContactItemDatabase()
func configure(for domain: ContactProviderDomain) {
// TODO: If needed, configure your enumerator for the domain.
}
func enumerateContent(in page: ContactItemPage, for observer: ContactItemContentObserver) async {
// Send the current contacts to the observer.
observer.didEnumerate(self.database.contactItems)
// Signal that we are done with the enumeration.
observer.didFinishEnumeratingContent(upTo: "<currentDatabaseGenerationMarker>".data(using: .utf8)!)
}
func enumerateChanges(startingAt syncAnchor: ContactItemSyncAnchor, for observer: ContactItemChangeObserver) async {
// Send the changes to the observer.
observer.didUpdate(self.database.contactItems)
// In this demo we dont support "remove" altough it can be implemented by giving the identifiers for the deleted items
// observer.didDelete(self.database.deletedItemIdentifiers)
// Signal that we are done with the enumeration.
observer.didFinishEnumeratingChanges(
upTo: ContactItemSyncAnchor(generationMarker: "<lastChangeGenerationMarker>".data(using: .utf8)!, offset: 0),
moreComing: false
)
}
func invalidate() async {
// TODO: Stop the enumeration and cleanup as the extension will be terminated.
}
}
Wrap up
You can download the full project from https://github.com/0xWDG/ContactProviderDemo.
Conclusion
ContactProvider is a great way to add contacts to the user's address book without interferring with their (personal) address book.
The finish signals are a bit confusing, but once you get the hang of it, it is a great way to add contacts to the user's address book.
Resources:
- ContactProvider
- ContactProviderManager
- ContactProviderExtension
- ContactItemEnumerator
- ContactItemContentObserver
- ContactItemChangeObserver
- ContactItemSyncAnchor
- https://github.com/0xWDG/ContactProviderDemo
Read more
- Aurora Editor • 5 minutes reading time.
- Aurora Editor • 5 minutes reading time.
- iCloud Drive Tips & Tricks • 8 minutes reading time.
Share
Share Mastodon Twitter LinkedIn Facebook