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

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:

  1. Load Contacts in Background: Fetch contacts asynchronously in a Task to avoid blocking the UI
  2. Implement Proper Indexing: Delegate complex searching to the SDK’s built-in methods
  3. Use Pagination: For large contact lists, implement pagination to improve performance
  4. Filter Locally: Once contacts are loaded, perform filtering locally for a smoother experience
  5. Show Loading Indicators: Always display appropriate loading states during search operations

Best Practices

  1. Provide Clear Empty States: Show informative messages when no contacts match search criteria
  2. Implement Intelligent Searching: Search across multiple fields (name, phone, email)
  3. Support Partial Matching: Allow users to find contacts with partial text entry
  4. Offer Search Suggestions: Provide search suggestions based on recent searches
  5. Optimize Layout for Different Devices: Ensure your search UI works well on both iPhone and iPad

Troubleshooting

Common Issues

  1. Slow Search Performance

    • Implement local filtering after initial data load
    • Consider pagination for large contact lists
    • Use more specific search criteria
  2. Missing Search Results

    • Ensure contacts have been properly synced
    • Check that your search logic handles different text cases
    • Verify permissions to access contacts