One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Contact UI

View, create, and edit contacts in the system Contacts UI

One.iOS.CNContactViewController presents ContactsUI’s native contact card or editor inside a navigation controller. Reach for it when the user should see and edit a contact in Apple’s own UI; for silent reads and writes from your own screens, One.Contacts plain records are the better fit.

This is iOS only. Constructing any of the three classes off iOS throws, since ContactsUI has no counterpart elsewhere.

Usage

import { One } from 'one'
const draft = new One.iOS.CNMutableContact()
draft.givenName = 'Ada'
draft.familyName = 'Lovelace'
const controller = new One.iOS.CNContactViewController({ forNewContact: draft })
controller.contactStore = new One.iOS.CNContactStore()
controller.delegate = {
contactViewController: async (sender, { didCompleteWith }) => {
// undefined means Cancel; otherwise this is the native saved contact.
await sender.dismiss()
},
}
await controller.present()

The three initializer forms take { forContact }, { forUnknownContact }, or { forNewContact }. Pass undefined for an empty new contact. Set ios.infoPlist.NSContactsUsageDescription before prebuild; the contacts.usage string stamps the same key.

How it works

The delegate owns dismissal, as in ContactsUI. present resolves after presentation, before Save or Cancel. Completion adapts the SDK’s labeled contactViewController(_:didCompleteWith:) to a JavaScript result object: a nil contact becomes undefined, no saved contact is fabricated for cancellation, and completion delivers at most once per presentation. Interactive sheet dismissal ends presentation ownership without synthesizing a Save or Cancel result.

To display an existing contact, fetch the SDK’s required keys first. The shared Contacts service can request permission or find identifiers, but its plain records are not native contacts:

import { One } from 'one'
async function viewContact(identifier: string) {
await One.Contacts.requestPermission()
const store = new One.iOS.CNContactStore()
const contact = await store.unifiedContact({
withIdentifier: identifier,
keysToFetch: [One.iOS.CNContactViewController.descriptorForRequiredKeys()],
})
const viewer = new One.iOS.CNContactViewController({ forContact: contact })
viewer.contactStore = store
viewer.allowsEditing = false
await viewer.present()
}

unifiedContact adapts Swift’s throwing fetch to a promise and labeled arguments to an options object. keysToFetch takes SDK string keys and native key descriptors. The initializers reject incomplete fetches before ContactsUI can raise a missing-property exception. Fetching uses native authorization, including limited contacts access; request access with One.Contacts.requestPermission.

Results retain native contacts. The bound subset covers the identifier, name and organization fields, isKeyAvailable, and mutableCopy. A mutable copy is independent and keeps the SDK contact identifier, with SDK empty-string defaults. Reading or changing an unfetched field throws; changing an immutable contact needs mutableCopy first. Phone numbers, addresses, images, dates, and other properties stay editable in the native UI but are not exposed by this subset.

The card keeps SDK defaults for its initializer mode. Configure it with displayedPropertyKeys, alternateName, message, allowsEditing, allowsActions, shouldShowLinkedContacts, and highlightProperty. Assigning contactStore = undefined clears the SDK store. Parent groups and containers are not bound. The synchronous shouldPerformDefaultActionFor delegate method is not bound because JavaScript callbacks run on a different thread from UIKit; its native default stays in effect, and allowsActions can disable actions.

API reference

Contact valuesTypeDescription
identifierstringNative contact identifier.
stringNative contact identifier.
namePrefixstringName prefix.
stringName prefix.
givenNamestringGiven name.
stringGiven name.
middleNamestringMiddle name.
stringMiddle name.
familyNamestringFamily name.
stringFamily name.
nameSuffixstringName suffix.
stringName suffix.
nicknamestringNickname.
stringNickname.
organizationNamestringOrganization name.
stringOrganization name.
jobTitlestringJob title.
stringJob title.
isKeyAvailable(key: string) => booleanWhether the SDK fetched the property.
(key: string) => booleanWhether the SDK fetched the property.
mutableCopy() => CNMutableContactValueIndependent native mutable copy.
() => CNMutableContactValueIndependent native mutable copy.
Mutable contact fieldsTypeDescription
namePrefixstringMutable name prefix.
stringMutable name prefix.
givenNamestringMutable given name.
stringMutable given name.
middleNamestringMutable middle name.
stringMutable middle name.
familyNamestringMutable family name.
stringMutable family name.
nameSuffixstringMutable name suffix.
stringMutable name suffix.
nicknamestringMutable nickname.
stringMutable nickname.
organizationNamestringMutable organization name.
stringMutable organization name.
jobTitlestringMutable job title.
stringMutable job title.
StoreTypeDescription
unifiedContact(options: CNContactFetchOptions) => Promise<CNContactValue>Fetches a native contact with the requested keys.
(options: CNContactFetchOptions) => Promise<CNContactValue>Fetches a native contact with the requested keys.
Fetch argumentsTypeDescription
withIdentifierstringStored contact identifier.
stringStored contact identifier.
keysToFetch(string | CNKeyDescriptor)[]SDK string keys and required-key descriptors.
(string | CNKeyDescriptor)[]SDK string keys and required-key descriptors.
ControllerTypeDescription
contactCNContactValueNative displayed contact, readonly.
CNContactValueNative displayed contact, readonly.
delegateCNContactViewControllerDelegate | nullReceives saved contact or cancellation.
CNContactViewControllerDelegate | nullReceives saved contact or cancellation.
contactStoreCNContactStoreValue | undefinedSDK store used for edits and additions.
CNContactStoreValue | undefinedSDK store used for edits and additions.
displayedPropertyKeysstring[] | undefinedVisible SDK property keys; undefined shows all.
string[] | undefinedVisible SDK property keys; undefined shows all.
alternateNamestring | undefinedDisplay name when no contact name exists.
string | undefinedDisplay name when no contact name exists.
messagestring | undefinedMessage displayed under the name.
string | undefinedMessage displayed under the name.
allowsEditingbooleanSDK editing permission for the card.
booleanSDK editing permission for the card.
allowsActionsbooleanWhether native contact actions appear.
booleanWhether native contact actions appear.
shouldShowLinkedContactsbooleanWhether linked contacts appear.
booleanWhether linked contacts appear.
highlightProperty(withKey: string, identifier?: string) => voidHighlights a property or labeled-value identifier.
(withKey: string, identifier?: string) => voidHighlights a property or labeled-value identifier.
present(animated?: boolean) => Promise<void>Presents native navigation and contact UI; animated defaults to true.
(animated?: boolean) => Promise<void>Presents native navigation and contact UI; animated defaults to true.
dismiss(animated?: boolean) => Promise<void>Dismisses the native UI; animated defaults to true.
(animated?: boolean) => Promise<void>Dismisses the native UI; animated defaults to true.

Platform differences

iOSAndroidWeb
UIContacts cardThrowsThrows
StoreNative fetchThrowsThrows
EditingNative editorNoneNone

Limits and gotchas

Examples

Save a new contact from a draft

import { One } from 'one'
async function saveContact(givenName: string, familyName: string) {
const draft = new One.iOS.CNMutableContact()
draft.givenName = givenName
draft.familyName = familyName
const controller = new One.iOS.CNContactViewController({ forNewContact: draft })
controller.contactStore = new One.iOS.CNContactStore()
const saved = await new Promise<string | undefined>((resolve) => {
controller.delegate = {
contactViewController: async (sender, { didCompleteWith }) => {
await sender.dismiss()
resolve(didCompleteWith?.identifier)
},
}
void controller.present()
})
return saved
}

The promise bridges the delegate callback into async flow: an identifier on Save, undefined on Cancel or sheet dismissal. The store assignment lets the editor persist the new contact.

Show a read-only contact card

import { One } from 'one'
async function showCard(identifier: string) {
await One.Contacts.requestPermission()
const store = new One.iOS.CNContactStore()
const contact = await store.unifiedContact({
withIdentifier: identifier,
keysToFetch: [One.iOS.CNContactViewController.descriptorForRequiredKeys()],
})
const card = new One.iOS.CNContactViewController({ forContact: contact })
card.contactStore = store
card.allowsEditing = false
card.allowsActions = false
card.delegate = {
contactViewController: async (sender) => {
await sender.dismiss()
},
}
await card.present()
}

Disabling editing and actions turns the card into a pure viewer. The delegate still dismisses, since Done still needs its dismissal.

Edit this page on GitHub.