1

I have a few functions for accessing the names and emails of people with ABAddressBook. The functions append the names and emails into a connections array. The problem I'm facing is that when the user is asked for the first time to access their address book, and they tap yes, the array is loaded but the tableview isn't updated.

The second time the app is loaded (and they are not asked because they've already hit yes), the arrays are loaded AND the tableview is updated.

I can't figure out why this is the case. I don't think it has to do with reloadData() because it does work on the second try.

My thought is that there is something incorrect about the logic of the code, but I can't figure it out.

Code:

 func getAddressBookNames() {
        let authorizationStatus = ABAddressBookGetAuthorizationStatus()
        if (authorizationStatus == ABAuthorizationStatus.NotDetermined) {
            var emptyDictionary: CFDictionaryRef?
            var addressBook = !(ABAddressBookCreateWithOptions(emptyDictionary, nil) != nil)
            ABAddressBookRequestAccessWithCompletion(addressBook,{success, error in
                if success {
                    self.processContactNames();
                }
                else {
                    println("Unable to request access")
                }
            })
        } else if (authorizationStatus == ABAuthorizationStatus.Denied || authorizationStatus == ABAuthorizationStatus.Restricted) {
            println("access denied")
        } else if (authorizationStatus == ABAuthorizationStatus.Authorized) {
            println("access granted")
            processContactNames()
        }
    }

    func processContactNames()
    {
        var errorRef: Unmanaged<CFError>?
        var addressBook: ABAddressBookRef? = extractABAddressBookRef(ABAddressBookCreateWithOptions(nil, &errorRef))
        println(addressBook)
        var contactList: NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
        println("records in the array \(contactList.count)")

        for record:ABRecordRef in contactList {
            processAddressbookRecord(record)
        }
    }

    func processAddressbookRecord(addressBookRecord: ABRecordRef) {
        var contactName: String = ABRecordCopyCompositeName(addressBookRecord).takeRetainedValue() as NSString
        connections.append("\(contactName) is an aquaintance")
        connectionDetails.append("Learned from Contacts")
        connectionCounter.text = "\(connections.count) Connections"
        processEmail(addressBookRecord, contactNameForEmail: contactName)
    }

    func processEmail(addressBookRecord: ABRecordRef, contactNameForEmail: String) {
        let emailArray:ABMultiValueRef = extractABEmailRef(ABRecordCopyValue(addressBookRecord, kABPersonEmailProperty))!
        for (var j = 0; j < ABMultiValueGetCount(emailArray); ++j) {
            var emailAdd = ABMultiValueCopyValueAtIndex(emailArray, j)
            var emailString = extractABEmailAddress(emailAdd)
            connections.append("\(contactNameForEmail)'s email is \(emailString!)")
            connectionDetails.append("Learned from Contacts")
            connectionCounter.text = "\(connections.count) Connections"
        }
    }

    func extractABAddressBookRef(abRef: Unmanaged<ABAddressBookRef>!) -> ABAddressBookRef? {
        if let ab = abRef {
            return Unmanaged<NSObject>.fromOpaque(ab.toOpaque()).takeUnretainedValue()
        }
        return nil
    }

    func extractABEmailRef (abEmailRef: Unmanaged<ABMultiValueRef>!) -> ABMultiValueRef? {
        if let ab = abEmailRef {
            return Unmanaged<NSObject>.fromOpaque(ab.toOpaque()).takeUnretainedValue()
        }
        return nil
    }

    func extractABEmailAddress (abEmailAddress: Unmanaged<AnyObject>!) -> String? {
        if let ab = abEmailAddress {
            return Unmanaged.fromOpaque(abEmailAddress.toOpaque()).takeUnretainedValue() as CFStringRef
        }
        return nil
    }

1 Answers1

0

It does have to do with reloadData(). It has to do with the fact that you are not calling it. Call it!

The important thing to understand is that ABAddressBookRequestAccessWithCompletion is asynchronous. Thus, by the time your completion handler is called, the table view has loaded its data already - at a time when there was no data. Thus, the table is empty. It is up to you to tell the table view that things have changed, by calling reloadData(). This time, the table view will discover that there is data, and will display it.

However, there's a twist. Not only is the completion handler asynchronous - it is also threaded. Pay careful attention to the docs here:

The completion handler is called on an arbitrary queue. If your app uses an address book throughout the app, you are responsible for ensuring that all usage of that address book is dispatched to a single queue to ensure correct thread-safe operation.

You are not doing that. You need, in your completion handler, immediately to jump onto the main thread. Now everything will happen in good order - just call reloadData() at the end and all will be well.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I've tried calling reloadData() after every possible point I could think of! I just tried calling it again after `if success processContactNames()`. Still didn't work. Where do you think I should call it? –  Dec 31 '14 at 17:59
  • Sorry, you also have a threading problem. I'll update my answer. – matt Dec 31 '14 at 18:00