WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Fetch CNContacts and update UI --> Blocking UI while fetching

Forums > SwiftUI

hello! i use a navigationBarButton to request a fetch of the contatcs stored in the device. in general it's working, but when the database is big (like on my real device) it blocks the fetchReuest button (it blocks the whole UI!). at the request i use DisppatchQueue.main.async, but still no improvement. do you have an idea, what i am doing wrong?

all my code is still on trial basis, so not all is nicely done yet.

thanks in advance for your support and help!

import SwiftUI
import Contacts

struct Contact: Identifiable, Hashable {
    var id = UUID()
    var firstName: String //= "No firstName"
    var lastName: String //= "No lastName"
    var phoneNumbers: [String] //= []
    var emailAddresses: [String]// = []

}

class FetchedContacts: ObservableObject, Identifiable {

    @Published var contacts = [Contact]()

    func fetchContacts() {
        contacts.removeAll()
        let store = CNContactStore()
        store.requestAccess(for: .contacts) { (granted, error) in
            if let error = error {
                print("failed to request access", error)
                return
            }
            if granted {
                let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey]
                let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
                do {
                    try store.enumerateContacts(with: request, usingBlock: { (contact, stopPointer) in

                 //       DispatchQueue.main.async {
                            self.contacts.append(Contact(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: contact.phoneNumbers.map { $0.value.stringValue }, emailAddresses: contact.emailAddresses.map { $0.value as String }
                            ))

                            self.contacts.sort(by: { $0.firstName < $1.firstName })
                   //     }
                    })

                } catch let error {
                    print("Failed to enumerate contact", error)
                }

            } else {
                print("access denied")
            }
        }
    }
}

struct ContentView: View {

    @ObservedObject var contacts = FetchedContacts()
    @State private var pick = false
    @State private var searchText: String = ""

    var body: some View {

        NavigationView {
            VStack {
                SearchBar(text: $searchText)
                List {

                    ForEach(contacts.contacts.filter {
                        searchText.isEmpty ? true : ($0.firstName + " " + $0.lastName).lowercased().contains(searchText.lowercased())
                    }) { contact in

                        VStack{
                            NavigationLink(destination: DetailedContactView(contact: contact)){

                                HStack{
                                    Text(contact.firstName)
                                    Text(contact.lastName)
                                }
                            }
                        }

                    }
                }
                .padding()
                .navigationTitle("Contacts")
                .navigationBarItems(trailing: Button("Fetch Contacts") {

                    DispatchQueue.main.async {
                        contacts.fetchContacts()
                    }

                })
            }
        }
    }

}
struct SearchBar: UIViewRepresentable {

    @Binding var text: String

    class Coordinator: NSObject, UISearchBarDelegate {

        @Binding var text: String

        init(text: Binding<String>) {
            _text = text
        }

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            if searchText != "" {
                searchBar.showsCancelButton = true
            }
                text = searchText

        }

        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            searchBar.text = ""
            searchBar.resignFirstResponder()
            searchBar.showsCancelButton = false
            text = ""
        }

    }

    func makeCoordinator() -> SearchBar.Coordinator {
        return Coordinator(text: $text)
    }

    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        searchBar.searchBarStyle = .minimal
        searchBar.autocapitalizationType = .none
        return searchBar
    }

    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
        uiView.text = text
    }

}

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}
struct DetailedContactView: View {

    var contact: Contact

    var body: some View {
        VStack{
            HStack{
                Text(contact.firstName)
                Text(contact.lastName)
            }
            ForEach(contact.phoneNumbers, id:\.self) { number in
                Text(number)
            }
            ForEach(contact.emailAddresses, id:\.self) { email in
                Text(email)
            }
        }
    }
}

1      

Hey I know this is a little late but I've recently needed to support the Contacts framework and created a very SwiftUI-friendly API that matches what you find when using Apple's own CoreData APIs. As such, I'm posting this here in case it helps you or others. Its open source so feel free to use in your personal or commercial apps :)

Example:

// This willl automatically refresh your view (with animation by default) 
// even if changes are observed from another device 😬
@FetchContactsList private var contacts

https://github.com/SwiftUI-Plus/Connections

1      

@shaps80

I am trying to use your framework. I have a TabView and i only want to request access to contacts when tapping on the second tab. However, when I launch the app all i get is a white screen and it never asks me to access contacts unless i kill rhe app. I think it does this because the 2 tab having the @FetchContactList is called when the TabView loads. Not sure on that.

How do i only request access when tapping the 2 tab with your framework?

Any help would be appreciated.

   

What you could do is to use tags and a state variable for your Tabs. https://www.hackingwithswift.com/books/ios-swiftui/creating-tabs-with-tabview-and-tabitem

Then you could use the onChange modifier for your state and only call the method when Tab 2 is selected.

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.