0

Please forgive me for my lack of knowledge, I'm trying to make my first iOS App, and my goal is to import all of my contacts into a swiftui view:

//ContentView.swift
import SwiftUI

struct ContentView: View {
    var contact = contactData
    @ObservedObject var contacts = ContactList()

    var body: some View {
        NavigationView {
            List {
                ForEach(contact) { item in
                    VStack(alignment: .leading) {
                        HStack {
                            Text(item.contactName)
                        }
                    }
                }
                .onDelete { index in
                    self.contacts.contact.remove(at: index.first!)
                }
            }
            .navigationBarTitle(Text("Contacts"))
            .navigationBarItems(
                trailing: EditButton()
            )
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

struct Contacts: Identifiable {
    var id = UUID()
    var contactName: String
}

let contactData = [
    Contacts(contactName: "Foo Bar"),
    Contacts(contactName: "Johnny Appleseed")
]

and

//ContactList.swift
import Combine

class ContactList: ObservableObject {
    @Published var contact: [Contacts] = contactData
}

Using the Combine API and the .onDelete function, I would like to delete multiple contacts (currently not a feature in iOS), then return them back into the contacts app.

I'm stuck though at pulling in the contact list, and I've tried multiple different ways of doing this with Swift like: Fetching all contacts in iOS Swift?

var contacts = [CNContact]()
let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(.FullName)]
let request = CNContactFetchRequest(keysToFetch: keys)

do {
    try self.contactStore.enumerateContactsWithFetchRequest(request) {             
        (contact, stop) in
        // Array containing all unified contacts from everywhere
        contacts.append(contact)
    }
} 
catch {
    print("unable to fetch contacts")
}

But that hasn't seemed to work properly without throwing a bunch of errors. I'm not so worried about deletion at the moment, but more primarily focused on just importing the contacts.

Does anyone know if there's a good way to do this using SwiftUI and the Contacts API? or can at least point me in the right direction? I understand I am on a Xcode 11 Beta 5, which may cause problems with deprecation of different API's, but it seems that the Contact API has been relatively unchanged in Xcode 11.

Brett Holmes
  • 397
  • 5
  • 20
  • 1
    A hopefully polite suggestion? `SwiftUI` *requires* iOS 13, where `UIKit` doesn't. Taking this along with how virtually *all* of your objective to work with contacts actually belong with your model - which shouldn't be any part of your UI, don't. Don't work with SwiftUI. Get your contact *logic* working first - and there is much more documentation for UIKit because it's been around for over a decade. Concern yourself with that first - the logic - and only *then* concern yourself with the UI. And that's what SwiftUI really is - only a method of *delivering* your logic. –  Aug 17 '19 at 03:36

1 Answers1

6

You usually use ObservableObject for model/controller interaction. Making them published just allows me to assign the result to the variable.

It currently starts the fetch inside onAppear async since it would otherwise block UI loading and asking for Contacts wouldn't work.

What is missing is better authorization handling (see this), deletion/creation, refresh (calling fetch somewhere else) and more comprehensive features.

A Conditional allows me to show the contacts or an error, which makes for quickly spotting errors.

Note: You have to add Privacy Contacts description into your info.plist for it to not crash. It should otherwise work without changing anything else.

Note 2: Since SwiftUI only the wiring and and the UI changed as @dfd said. How to retrieve contacts still works exactly the same.

import Contacts
import SwiftUI
import os

class ContactStore: ObservableObject {
    @Published var contacts: [CNContact] = []
    @Published var error: Error? = nil

    func fetch() {
        os_log("Fetching contacts")
        do {
            let store = CNContactStore()
            let keysToFetch = [CNContactGivenNameKey as CNKeyDescriptor,
                               CNContactMiddleNameKey as CNKeyDescriptor,
                               CNContactFamilyNameKey as CNKeyDescriptor,
                               CNContactImageDataAvailableKey as CNKeyDescriptor,
                               CNContactImageDataKey as CNKeyDescriptor]
            os_log("Fetching contacts: now")
            let containerId = store.defaultContainerIdentifier()
            let predicate = CNContact.predicateForContactsInContainer(withIdentifier: containerId)
            let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
            os_log("Fetching contacts: succesfull with count = %d", contacts.count)
            self.contacts = contacts
        } catch {
            os_log("Fetching contacts: failed with %@", error.localizedDescription)
            self.error = error
        }
    }
}

extension CNContact: Identifiable {
    var name: String {
        return [givenName, middleName, familyName].filter{ $0.count > 0}.joined(separator: " ")
    }
}

struct ContactsView: View {
    @EnvironmentObject var store: ContactStore

    var body: some View {
        VStack{
            Text("Contacts")
            if store.error == nil {
                List(store.contacts) { (contact: CNContact) in
                    return Text(contact.name)
                }.onAppear{
                    DispatchQueue.main.async {
                        self.store.fetch()
                    }
                }
            } else {
                Text("error: \(store.error!.localizedDescription)")
            }
        }
    }
}

struct ContactsViewOrError: View {
    var body: some View {
        ContactsView().environmentObject(ContactStore())
    }
}
Fabian
  • 5,040
  • 2
  • 23
  • 35
  • Thank you! This worked really well, and shows me that I need to learn a lot more of the logic like @dfd said. Do you know why there might be blank contacts in my list? It did return all of them successfully. – Brett Holmes Aug 17 '19 at 15:14
  • 1
    @BrettHolmes the fetch only fetches the given keys and of that only names are being put into the Text-view. It just means that that entry does not have anything set in given name, middle name, or family name. Through fetching more fields and printing them you might get an idea for what it is used,f it at all. I've got such an entry too :D. Or look into the contacts app if such an entry exists. – Fabian Aug 17 '19 at 15:41