Get Started
Features
Services
Services
Search UI
Services
Search UI
Building search interfaces with the ContactsManager SDK
Search UI
The ContactsManager SDK provides powerful contact search capabilities that you can integrate into your app. This guide explains how to implement contact search and build intuitive search interfaces.
Basic Contact Searching
To search for contacts, you can use the fetchContacts
method with appropriate filters:
do {
// Fetch all contacts
let allContacts = try await ContactsService.shared.fetchContacts(
fieldType: .any
)
// Fetch only contacts with phone numbers
let phoneContacts = try await ContactsService.shared.fetchContacts(
fieldType: .phone
)
// Fetch only contacts with email addresses
let emailContacts = try await ContactsService.shared.fetchContacts(
fieldType: .email
)
// Fetch contacts with notes
let contactsWithNotes = try await ContactsService.shared.fetchContacts(
fieldType: .notes
)
} catch {
print("Error fetching contacts: \(error.localizedDescription)")
}
Implementing a Search UI
Here’s an example of building a contact search interface with SwiftUI:
struct ContactSearchView: View {
@State private var searchText = ""
@State private var allContacts: [Contact] = []
@State private var isLoading = true
@State private var error: Error?
var body: some View {
NavigationView {
VStack {
if isLoading {
ProgressView("Loading contacts...")
} else if let error = error {
VStack {
Text("Error loading contacts")
.font(.headline)
Text(error.localizedDescription)
.foregroundColor(.red)
Button("Try Again") {
loadContacts()
}
.padding()
}
} else {
List(filteredContacts, id: \.id) { contact in
ContactRow(contact: contact)
}
.listStyle(PlainListStyle())
.overlay(
Group {
if filteredContacts.isEmpty {
if searchText.isEmpty {
Text("No contacts found")
} else {
Text("No contacts match '\(searchText)'")
}
}
}
)
}
}
.navigationTitle("Contacts")
.searchable(text: $searchText, prompt: "Search contacts")
.onAppear {
loadContacts()
}
}
}
var filteredContacts: [Contact] {
if searchText.isEmpty {
return allContacts
} else {
return allContacts.filter { contact in
// Search in name
if let displayName = contact.displayName?.lowercased(),
displayName.contains(searchText.lowercased()) {
return true
}
// Search in phone numbers
for phone in contact.phoneNumbers {
if phone.value.lowercased().contains(
searchText.lowercased()
) {
return true
}
}
// Search in email addresses
for email in contact.emailAddresses {
if email.value.lowercased().contains(
searchText.lowercased()
) {
return true
}
}
return false
}
}
}
private func loadContacts() {
isLoading = true
error = nil
Task {
do {
let contacts = try await ContactsService.shared.fetchContacts(
fieldType: .any
)
await MainActor.run {
self.allContacts = contacts
self.isLoading = false
}
} catch {
await MainActor.run {
self.error = error
self.isLoading = false
}
}
}
}
}
struct ContactRow: View {
let contact: Contact
var body: some View {
HStack {
// Contact avatar
if let imageData = contact.thumbnailImageData,
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 50, height: 50)
.clipShape(Circle())
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 50, height: 50)
.overlay(
Text(contact.initials ?? "?")
.font(.title2)
.foregroundColor(.gray)
)
}
// Contact info
VStack(alignment: .leading, spacing: 4) {
Text(contact.displayName ?? "Unknown")
.font(.headline)
if let phone = contact.phoneNumbers.first?.value {
Text(phone)
.font(.subheadline)
.foregroundColor(.gray)
} else if let email = contact.emailAddresses.first?.value {
Text(email)
.font(.subheadline)
.foregroundColor(.gray)
}
}
Spacer()
}
.padding(.vertical, 4)
}
}
Advanced Search Features
Segmented Search
You can build a segmented search interface to filter contacts by category:
struct SegmentedContactSearchView: View {
@State private var searchText = ""
@State private var selectedSegment = 0
@State private var contacts: [Contact] = []
@State private var isLoading = true
@State private var error: Error?
private let segments = ["All", "Phone", "Email"]
var body: some View {
VStack {
Picker("Contact Type", selection: $selectedSegment) {
ForEach(0..<segments.count, id: \.self) { index in
Text(segments[index])
}
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
.onChange(of: selectedSegment) { _ in
loadContacts()
}
if isLoading {
ProgressView("Loading contacts...")
} else if let error = error {
VStack {
Text("Error loading contacts")
.font(.headline)
Text(error.localizedDescription)
.foregroundColor(.red)
Button("Try Again") {
loadContacts()
}
.padding()
}
} else {
List(filteredContacts, id: \.id) { contact in
ContactRow(contact: contact)
}
.listStyle(PlainListStyle())
}
}
.navigationTitle("Search Contacts")
.searchable(
text: $searchText,
prompt: "Search \(segments[selectedSegment].lowercased()) contacts"
)
.onAppear {
loadContacts()
}
}
var filteredContacts: [Contact] {
if searchText.isEmpty {
return contacts
} else {
return contacts.filter { contact in
switch selectedSegment {
case 1: // Phone
return contact.phoneNumbers.contains { phone in
phone.value.lowercased().contains(
searchText.lowercased()
)
}
case 2: // Email
return contact.emailAddresses.contains { email in
email.value.lowercased().contains(
searchText.lowercased()
)
}
default: // All
// Search in name
if let displayName = contact.displayName?.lowercased(),
displayName.contains(searchText.lowercased()) {
return true
}
// Search in phone numbers
for phone in contact.phoneNumbers {
if phone.value.lowercased().contains(
searchText.lowercased()
) {
return true
}
}
// Search in email addresses
for email in contact.emailAddresses {
if email.value.lowercased().contains(
searchText.lowercased()
) {
return true
}
}
return false
}
}
}
}
private func loadContacts() {
isLoading = true
error = nil
Task {
do {
// Load contacts based on selected segment
let fieldType: ContactFieldType = {
switch selectedSegment {
case 1:
return .phone
case 2:
return .email
default:
return .any
}
}()
let loadedContacts = try await ContactsService.shared.fetchContacts(
fieldType: fieldType
)
await MainActor.run {
self.contacts = loadedContacts
self.isLoading = false
}
} catch {
await MainActor.run {
self.error = error
self.isLoading = false
}
}
}
}
}
Contact Detail View
When a user selects a contact, show a detailed view:
Swift
struct ContactDetailView: View {
let contact: Contact
@State private var isFollowing = false
@State private var isLoading = true
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// Header with avatar and name
HStack(spacing: 16) {
// Avatar
if let imageData = contact.thumbnailImageData,
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 100, height: 100)
.overlay(
Text(contact.initials ?? "?")
.font(.largeTitle)
.foregroundColor(.gray)
)
}
VStack(alignment: .leading, spacing: 4) {
Text(contact.displayName ?? "Unknown")
.font(.title)
.fontWeight(.bold)
if let organization = contact.organizationName {
Text(organization)
.font(.subheadline)
.foregroundColor(.gray)
}
if let jobTitle = contact.jobTitle {
Text(jobTitle)
.font(.subheadline)
.foregroundColor(.gray)
}
HStack {
Button(action: {
toggleFollow()
}) {
Text(isFollowing ? "Following" : "Follow")
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(isFollowing ? Color.gray.opacity(0.2) : Color.blue)
.foregroundColor(isFollowing ? .primary : .white)
.cornerRadius(16)
}
Button(action: {
// Message action
}) {
Text("Message")
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(16)
}
}
.padding(.top, 4)
}
}
.padding()
Divider()
// Contact information sections
Group {
// Phone numbers
if !contact.phoneNumbers.isEmpty {
ContactInfoSection(title: "Phone") {
ForEach(contact.phoneNumbers, id: \.id) { phone in
ContactInfoRow(
icon: "phone.fill",
label: phone.value,
subtitle: phone.type.label
)
}
}
}
// Email addresses
if !contact.emailAddresses.isEmpty {
ContactInfoSection(title: "Email") {
ForEach(contact.emailAddresses, id: \.id) { email in
ContactInfoRow(
icon: "envelope.fill",
label: email.value,
subtitle: email.type.label
)
}
}
}
// Addresses
if !contact.addresses.isEmpty {
ContactInfoSection(title: "Address") {
ForEach(contact.addresses, id: \.id) { address in
ContactInfoRow(
icon: "location.fill",
label: formatAddress(address),
subtitle: address.type.label
)
}
}
}
// Social profiles
if !contact.socialProfiles.isEmpty {
ContactInfoSection(title: "Social") {
ForEach(contact.socialProfiles, id: \.id) { profile in
ContactInfoRow(
icon: "person.crop.circle.fill",
label: profile.value,
subtitle: profile.type.label
)
}
}
}
// Notes
if let notes = contact.notes, !notes.isEmpty {
ContactInfoSection(title: "Notes") {
Text(notes)
.font(.body)
.padding(.horizontal)
.padding(.bottom, 8)
}
}
}
}
}
.navigationTitle("Contact")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
checkFollowStatus()
}
}
private func checkFollowStatus() {
Task {
do {
let status = try await ContactsService.shared.socialService.isFollowingContact(
followedId: contact.id
)
await MainActor.run {
isFollowing = status.isFollowing
isLoading = false
}
} catch {
print("Error checking follow status: \(error.localizedDescription)")
await MainActor.run {
isLoading = false
}
}
}
}
private func toggleFollow() {
Task {
do {
if isFollowing {
let result = try await ContactsService.shared.socialService.unfollowContact(
followedId: contact.id
)
if let success = result.success, success {
await MainActor.run {
isFollowing = false
}
}
} else {
let result = try await ContactsService.shared.socialService.followContact(
followedId: contact.id,
contactId: contact.id
)
if let success = result.success, success {
await MainActor.run {
isFollowing = true
}
}
}
} catch {
print("Error toggling follow: \(error.localizedDescription)")
}
}
}
private func formatAddress(_ address: ContactAddress) -> String {
var components: [String] = []
if let street = address.street, !street.isEmpty {
components.append(street)
}
if let city = address.city, !city.isEmpty {
components.append(city)
}
if let state = address.state, !state.isEmpty {
components.append(state)
}
if let postalCode = address.postalCode, !postalCode.isEmpty {
components.append(postalCode)
}
if let country = address.country, !country.isEmpty {
components.append(country)
}
return components.joined(separator: ", ")
}
}
struct ContactInfoSection<Content: View>: View {
let title: String
let content: Content
init(title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
.padding(.horizontal)
.padding(.top, 8)
content
Divider()
}
}
}
struct ContactInfoRow: View {
let icon: String
let label: String
let subtitle: String?
var body: some View {
HStack {
Image(systemName: icon)
.frame(width: 24, height: 24)
.foregroundColor(.blue)
VStack(alignment: .leading) {
Text(label)
.font(.body)
if let subtitle = subtitle {
Text(subtitle)
.font(.caption)
.foregroundColor(.gray)
}
}
Spacer()
}
.padding(.horizontal)
.padding(.vertical, 4)
}
}
Search Performance Tips
To ensure a responsive search experience:
- Load Contacts in Background: Fetch contacts asynchronously in a Task to avoid blocking the UI
- Implement Proper Indexing: Delegate complex searching to the SDK’s built-in methods
- Use Pagination: For large contact lists, implement pagination to improve performance
- Filter Locally: Once contacts are loaded, perform filtering locally for a smoother experience
- Show Loading Indicators: Always display appropriate loading states during search operations
Best Practices
- Provide Clear Empty States: Show informative messages when no contacts match search criteria
- Implement Intelligent Searching: Search across multiple fields (name, phone, email)
- Support Partial Matching: Allow users to find contacts with partial text entry
- Offer Search Suggestions: Provide search suggestions based on recent searches
- Optimize Layout for Different Devices: Ensure your search UI works well on both iPhone and iPad
Troubleshooting
Common Issues
-
Slow Search Performance
- Implement local filtering after initial data load
- Consider pagination for large contact lists
- Use more specific search criteria
-
Missing Search Results
- Ensure contacts have been properly synced
- Check that your search logic handles different text cases
- Verify permissions to access contacts