Loading a UITableView with an array from ABAddressBook (Swift) - uitableview

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
}

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.

Related

Swift Func called after expected

I have a button setup so that it saves a CK record based on a users choice from a different part of the UI. Once the function is called the CKRecord is saved in a variable. The next operation the code should take is unwrapping that variable and using it to edit and save the CK record. The Issue is the function I call first, loadChallengeRecord(), isn't the first operation made when the button is pressed. Instead the unwrapping function is run first which is causing the program to exit the unwrap function because the record is nil, and then the loadChallengeRecord() function is called late. Here is the example:
func loadChallengeRecord() {
if let unwrapped = existingChallengeToDetails {
recordID = CKRecord.ID(recordName: unwrapped, zoneID: zone)
publicDatabase.fetch(withRecordID: recordID!) { (record, error) in
if record != nil {
self.currentChallenge = record
} else {
print("error fetching challenge record from server")
}
}
}
}
#IBAction func btnVote(_ sender: Any) {
// load record and save it to var existingChallengeToDetails
loadChallengeRecord()
if let unwrapped = existingChallengeToDetails { }// edit and save record
else { // error }
What am i doing wrong? How can i fix this? Can I denote a priority for these functions to run?
Write your function with completion handler like this
func loadChallengeRecord(completion:#escaping ()->Void) {
if let unwrapped = existingChallengeToDetails {
recordID = CKRecord.ID(recordName: unwrapped, zoneID: zone)
publicDatabase.fetch(withRecordID: recordID!) { (record, error) in
defer { completion }
if record != nil {
self.currentChallenge = record
} else {
print("error fetching challenge record from server")
}
}
}
}
Use it like this ... when it returns completion you can do other stuff dependent on this
loadChallengeRecord {
// do your stuff here
}
The easiest solution is to do the things you have to do in the (asynchronous) completion handler
func loadChallengeRecord() {
guard let unwrapped = existingChallengeToDetails else { return }
recordID = CKRecord.ID(recordName: unwrapped, zoneID: zone)
publicDatabase.fetch(withRecordID: recordID!) { (record, error) in
if let record = record {
self.currentChallenge = record
// edit and save record
} else {
print("error fetching challenge record from server", error!)
}
}
}
#IBAction func btnVote(_ sender: Any) {
// load record and save it to var existingChallengeToDetails
loadChallengeRecord()
}
The other two answers worked for the specific case, and work for many causes, but they weren't exactly what I was looking for. I kept running to similar issues and after some research I finally found what I was looking for: Semaphores
Here is a basic explanation:
func doSomething() {
let semaphore = DispatchSemaphore(value: 0) // Setup the semaphore to value:0
var x = 1
var y = 2
if y != 0 {
var sum = x + y
semaphore.signal(). // Sends signal that code is complete
// the code can continue from where semaphore.wait() is located.
}
semaphore.wait() // Wait for semaphore.signal() to fire, then continue to return
return sum // only after semaphore.signal() fires will this code run
}

App freeze with DispatchSemaphore wait()

I have created a function getFriends that reads a User's friendlist from firestore and puts each friend in a LocalUser object (which is my custom user class) in order to display the friendlist in a tableview. I need the DispatchSemaphore.wait() because I need the for loop to iterate only when the completion handler inside the for loop is called.
When loading the view, the app freezes. I know that the problem is that semaphore.wait() is called in the main thread. However, from reading DispatchQueue-tutorials I still don't understand how to fix this in my case.
Also: do you see any easier ways to implement what I want to do?
This is my call to the function in viewDidLoad():
self.getFriends() { (friends) in
self.foundFriends = friends
self.friendsTable.reloadData()
}
And the function getFriends:
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
}
}
friendsUID is a dict with each friend's uid as a key and true as the value. Since I only need the keys, I store them in the array friendsIdents. Function getUser searches the passed uid in firestore and creates the corresponding LocalUser (usr). This finally gets appended in friends array.
You should almost never have a semaphore.wait() on the main thread. Unless you expect to wait for < 10ms.
Instead, consider dispatching your friends list processing to a background thread. The background thread can perform the dispatch to your database/api and wait() without blocking the main thread.
Just make sure to use DispatchQueue.main.async {} from that thread if you need to trigger any UI work.
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
DispatchQueue.global(qos: .userInitiated).async {
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
// Insert here a DispatchQueue.main.async {} if you need something to happen
// on the main queue after you are done processing all entries
}
}

How to return a bool to the outside function when you have nested functions in Swift?

So I have a function I have that has an inner function that connects to a Firebase database. the function returns a bool but I need the to return true or false based on whats inside the Firebase database. So basically I need to return true or false inside of the Firebase function the problem is it thinks that I am trying to return to the Firebase function when I am actually trying to return to the outside function a simple example is this
func someOtherFunc(name: String, lastname: String){
}
func someFunc(name: String, lastname: String) -> bool {
someOtherFunc(name: name, lastname: lastname) {
if name == "George" {
return true
} else {
return false // will not work because some other func does not return a value
}
}
}
here is the code that I need fixed which is a little more complicated than the above function because the inner Firebase function runs asynchronously(in the background) and so all the code inside the function needs to run synchronously to return the correct value
Here is my function
func takeAwayMoney(_ howMuch: String) -> Bool{
if let notMuch = Int(howMuch) {
let userID = FIRAuth.auth()?.currentUser?.uid
datRef.child("User").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let money = value?["money"] as? String ?? ""
//convert money to int
if let conMoney = Int(money) {
var conMoreMoney = conMoney
if conMoreMoney < notMuch {
print(" sorry you don't have enough money")
return false
} else {
conMoreMoney -= notMuch
let values = ["money": String(conMoreMoney)]
//update the users money
self.datRef.child("User").child(userID!).updateChildValues(values)
return true //doesn't work because of example above
}
}
// ...
}) { (error) in
print(error.localizedDescription)
}
}
}
This code does not compile because there is no return values to the main function.
I know the really hard way to fix this would be to initialize values at the top of the function that would be the money of the user then check it after dispatching it for a couple of seconds then you could return the values, but I know there must be another way because this way would cause a lot of problems.
The root cause is that takeAwayMoney is synchronous, but it uses observeSingleEvent, which is asynchronous.
The "right" way to solve this is to make takeAwayMoney return Void, but take a completion function that will give you the bool asynchronously, like this:
func takeAwayMoney(_ howMuch: String, completion: #escaping (Bool)->()) -> Void {
/// When you want to "return" the bool, call the completion. e.g.:
// ...
if conMoreMoney < notMuch {
print(" sorry you don't have enough money")
completion(false)
return // if you want to stop processing
}
// ...
}
If you cannot do this, then takeMoney needs to wait for the completion to finish and you use semaphores to have them communicate with each other. You do not just wait a couple of seconds. See this for an example:
Semaphores to run asynchronous method synchronously

How to stop viewWillAppear from completing until a function has finished

I'm pretty new to IOS Application Development.
I'm trying to stop viewWillAppear from finishing until after my function has finished working. How do I do that?
Here's viewWillAppear:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
checkFacts()
if reset != 0 {
print("removing all bird facts")
birdFacts.removeAll()
}
}
func checkFacts() {
let date = getDate()
var x: Bool = true
var ind: Int = 0
print("count is ", birdFacts.count)
while ind < birdFacts.count {
print("accessing each bird fact in checkFacts")
let imageAsset: CKAsset = birdFacts[ind].valueForKey("birdPicture") as! CKAsset
let image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
print(image)
if image == nil {
if (birdFacts[ind].valueForKey("sortingDate") != nil){
print("replacing fact")
print("accessing the sortingDate of current fact in checkFacts")
let sdate = birdFacts[ind].valueForKey("sortingDate") as! NSNumber
replaceFact(sdate, index: ind)
}
/*else {
birdFacts.removeAll()
print("removing all bird facts")
}*/
}
ind = ind + 1
print(ind)
}
self.saveFacts()
let y = checkRepeatingFacts()
if y {
print("removing all facts")
birdFacts.removeAll()
//allprevFacts(date, olddate: 0)
}
}
checkFacts references 2 others functions, but I'm not sure they're relevant here (but I will add them in if they are and I'm mistaken)
Instead of trying to alter or halt the application's actual lifecycle, why don't you try using a closure?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
checkFacts(){ Void in
if self.reset != 0 {
print("removing all bird facts")
birdFacts.removeAll()
}
}
}
func checkFacts(block: (()->Void)? = nil) {
let date = getDate()
var x: Bool = true
var ind: Int = 0
print("count is ", birdFacts.count)
while ind < birdFacts.count {
print("accessing each bird fact in checkFacts")
let imageAsset: CKAsset = birdFacts[ind].valueForKey("birdPicture") as! CKAsset
let image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
print(image)
if image == nil {
if (birdFacts[ind].valueForKey("sortingDate") != nil){
print("replacing fact")
print("accessing the sortingDate of current fact in checkFacts")
let sdate = birdFacts[ind].valueForKey("sortingDate") as! NSNumber
replaceFact(sdate, index: ind)
}
/*else {
birdFacts.removeAll()
print("removing all bird facts")
}*/
}
ind = ind + 1
print(ind)
}
self.saveFacts()
let y = checkRepeatingFacts()
if y {
print("removing all facts")
birdFacts.removeAll()
//allprevFacts(date, olddate: 0)
}
// CALL CODE IN CLOSURE LAST //
if let block = block {
block()
}
}
According to Apple Documentation:
Closures are self-contained blocks of functionality that can be passed around and used in your code.
Closures can capture and store references to any constants and variables from the context in which they are defined.
So by defining checkFacts() as: func checkFacts(block: (()->Void)? = nil){...} we can optionally pass in a block of code to be executed within the checkFacts() function.
The syntax block: (()->Void)? = nil means that we can take in a block of code that will return void, but if nothing is passed in, block will simply be nil. This allows us to call the function with or without the use of a closure.
By using:
if let block = block {
block()
}
we can safely call block(). If block comes back as nil, we pass over it and pretend like nothing happened. If block is not nil, we can execute the code contained within it, and go on our way.
One way we can pass our closure code into checkFacts() is by means of a trailing closure. A trailing closure looks like this:
checkFacts(){ Void in
if self.reset != 0 {
print("removing all bird facts")
birdFacts.removeAll()
}
}
Edit: Added syntax explanation.
So based on the comments, checkFacts is calling asynchronous iCloud operations that if they are not complete will result in null data that your view cannot manage.
Holding up viewWillAppear is not the way to manage this - that will just result in a user interface delay that will irritate your users.
Firstly, your view should be able to manage null data without crashing. Even when you solve this problem there may be other occasions when the data becomes bad and users hate crashes. So I recommend you fix that.
To fix the original problem: allow the view to load with unchecked data. Then trigger the checkData process and when it completes post an NSNotification. Make your view watch for that notification and redraw its contents when it occurs. Optionally, if you don't want your users to interact with unchecked data: disable appropriate controls and display an activity indicator until the notification occurs.

Accessing iOS Address Book with Swift: array count of zero

I am trying to write a simple method to ask a user for access to their address book and then print out the name of each person in the address book. I've seen a number of tutorials explaining how to do this in objective-C, but am having a hard time converting them to swift.
Here's what I've done so far. The below block runs in my viewDidLoad() method and checks to see whether the user has authorized access to the address book or not, if they have not authorized access yet, the first if-statement will ask for access. This section works as expected.
var emptyDictionary: CFDictionaryRef?
var addressBook: ABAddressBookRef?
if (ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.NotDetermined)
{
println("requesting access...")
addressBook = !ABAddressBookCreateWithOptions(emptyDictionary,nil)
ABAddressBookRequestAccessWithCompletion(addressBook,{success, error in
if success {
self.getContactNames();
}
else
{
println("error")
}
})
}
}
else if (ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.Denied || ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.Restricted)
{
println("access denied")
}
else if (ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.Authorized)
{
println("access granted")
getContactNames()
}
Once I know the user has granted access, I run the getContactNames() method which is below. After much back and forth, I was finally able to get this to compile by adding the takeRetainedValue() method in order to convert the array returned by ABAddressBookCopyArrayOfAllPeople from an unmanaged array to a managed array, this then allows me to convert the CFArrayRef to an NSArray.
The issue I'm running into is that the contactList array ends up having a count of 0 and the for loop therefore gets skipped. In my simulator, the address book has 6 or 7 records, so I would expect the array to be of that length. Any ideas?
func getContactNames()
{
addressBook = !ABAddressBookCreateWithOptions(emptyDictionary,nil)
var contactList: NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
println("records in the array \(contactList.count)") // returns 0
for record:ABRecordRef in contactList {
var contactPerson: ABRecordRef = record
var contactName: String = ABRecordCopyCompositeName(contactPerson).takeRetainedValue()
println ("contactName \(contactName)")
}
}
One additional point - if I use the ABAddressBookGetPersonCount method, it returns -1.
var count: CFIndex = ABAddressBookGetPersonCount(addressBook);
println("records in the array \(count)") // returns -1
Based on this link ABAddressBookGetPersonCount returns -1 in iOS, it seems that this function returning -1 could be related to permission not being granted, but I definitely have asked for permission in the code above (and granted it when I run the app in the simulator)
This is now all much simpler. The chief thing to watch out for is that if you create an ABAddressBook without authorization, you get an evil address book - it isn't nil but it isn't good for anything either. Here's how I currently recommend that you set up authorization status and request authorization if necessary:
var adbk : ABAddressBook!
func createAddressBook() -> Bool {
if self.adbk != nil {
return true
}
var err : Unmanaged<CFError>? = nil
let adbk : ABAddressBook? = ABAddressBookCreateWithOptions(nil, &err).takeRetainedValue()
if adbk == nil {
println(err)
self.adbk = nil
return false
}
self.adbk = adbk
return true
}
func determineStatus() -> Bool {
let status = ABAddressBookGetAuthorizationStatus()
switch status {
case .Authorized:
return self.createAddressBook()
case .NotDetermined:
var ok = false
ABAddressBookRequestAccessWithCompletion(nil) {
(granted:Bool, err:CFError!) in
dispatch_async(dispatch_get_main_queue()) {
if granted {
ok = self.createAddressBook()
}
}
}
if ok == true {
return true
}
self.adbk = nil
return false
case .Restricted:
self.adbk = nil
return false
case .Denied:
self.adbk = nil
return false
}
}
And here's how to cycle through all persons and print out their names:
func getContactNames() {
if !self.determineStatus() {
println("not authorized")
return
}
let people = ABAddressBookCopyArrayOfAllPeople(adbk).takeRetainedValue() as NSArray as [ABRecord]
for person in people {
println(ABRecordCopyCompositeName(person).takeRetainedValue())
}
}
There seems to be a bug either with the compiler or the framework where ABAddressBookRef is declared a typealias of AnyObject, but it needs to be NSObject in order to unwrap it from the Unmanaged<ABAddressBookRef>! returned by ABAddressBookCreateWithOptions. A workaround is to convert it to and from an opaque C pointer. The following code works, but it should probably be doing a lot more error checking (and there is also probably a better way of working around this issue):
var addressBook: ABAddressBookRef?
func extractABAddressBookRef(abRef: Unmanaged<ABAddressBookRef>!) -> ABAddressBookRef? {
if let ab = abRef {
return Unmanaged<NSObject>.fromOpaque(ab.toOpaque()).takeUnretainedValue()
}
return nil
}
func test() {
if (ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.NotDetermined) {
println("requesting access...")
var errorRef: Unmanaged<CFError>? = nil
addressBook = extractABAddressBookRef(ABAddressBookCreateWithOptions(nil, &errorRef))
ABAddressBookRequestAccessWithCompletion(addressBook, { success, error in
if success {
self.getContactNames()
}
else {
println("error")
}
})
}
else if (ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.Denied || ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.Restricted) {
println("access denied")
}
else if (ABAddressBookGetAuthorizationStatus() == ABAuthorizationStatus.Authorized) {
println("access granted")
self.getContactNames()
}
}
func getContactNames() {
var errorRef: Unmanaged<CFError>?
addressBook = extractABAddressBookRef(ABAddressBookCreateWithOptions(nil, &errorRef))
var contactList: NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
println("records in the array \(contactList.count)")
for record:ABRecordRef in contactList {
var contactPerson: ABRecordRef = record
var contactName: String = ABRecordCopyCompositeName(contactPerson).takeRetainedValue() as NSString
println ("contactName \(contactName)")
}
}
For those looking for the complete working solution, here is how to print out only the contact names, modifying the above code. Invoke getAddressBookNames() to access the address book, e.g. in the viewDidLoad() method.
func getAddressBookNames() {
let authorizationStatus = ABAddressBookGetAuthorizationStatus()
if (authorizationStatus == ABAuthorizationStatus.NotDetermined)
{
NSLog("requesting access...")
var emptyDictionary: CFDictionaryRef?
var addressBook = !ABAddressBookCreateWithOptions(emptyDictionary, nil)
ABAddressBookRequestAccessWithCompletion(addressBook,{success, error in
if success {
self.getContactNames();
}
else {
NSLog("unable to request access")
}
})
}
else if (authorizationStatus == ABAuthorizationStatus.Denied || authorizationStatus == ABAuthorizationStatus.Restricted) {
NSLog("access denied")
}
else if (authorizationStatus == ABAuthorizationStatus.Authorized) {
NSLog("access granted")
getContactNames()
}
}
func getContactNames()
{
var errorRef: Unmanaged<CFError>?
var addressBook: ABAddressBookRef? = extractABAddressBookRef(ABAddressBookCreateWithOptions(nil, &errorRef))
var contactList: NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
println("number of contacts: \(contactList.count)")
for record:ABRecordRef in contactList {
var contactName: String = ABRecordCopyCompositeName(record).takeRetainedValue() as NSString
NSLog("contactName: \(contactName)")
}
}
func extractABAddressBookRef(abRef: Unmanaged<ABAddressBookRef>!) -> ABAddressBookRef? {
if let ab = abRef {
return Unmanaged<NSObject>.fromOpaque(ab.toOpaque()).takeUnretainedValue()
}
return nil
}
And here is the complete code to access the contact names and emails - this is done using the helper methods defined in some of the other answers.
func getAddressBookNames() {
let authorizationStatus = ABAddressBookGetAuthorizationStatus()
if (authorizationStatus == ABAuthorizationStatus.NotDetermined)
{
NSLog("requesting access...")
var emptyDictionary: CFDictionaryRef?
var addressBook = !ABAddressBookCreateWithOptions(emptyDictionary, nil)
ABAddressBookRequestAccessWithCompletion(addressBook,{success, error in
if success {
self.processContactNames();
}
else {
NSLog("unable to request access")
}
})
}
else if (authorizationStatus == ABAuthorizationStatus.Denied || authorizationStatus == ABAuthorizationStatus.Restricted) {
NSLog("access denied")
}
else if (authorizationStatus == ABAuthorizationStatus.Authorized) {
NSLog("access granted")
processContactNames()
}
}
func processContactNames()
{
var errorRef: Unmanaged<CFError>?
var addressBook: ABAddressBookRef? = extractABAddressBookRef(ABAddressBookCreateWithOptions(nil, &errorRef))
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
NSLog("contactName: \(contactName)")
processEmail(addressBookRecord)
}
func processEmail(addressBookRecord: ABRecordRef) {
let emailArray:ABMultiValueRef = extractABEmailRef(ABRecordCopyValue(addressBookRecord, kABPersonEmailProperty))!
for (var j = 0; j < ABMultiValueGetCount(emailArray); ++j) {
var emailAdd = ABMultiValueCopyValueAtIndex(emailArray, j)
var myString = extractABEmailAddress(emailAdd)
NSLog("email: \(myString!)")
}
}
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
}
If anyone is also trying to get the email addresses of the contacts, I found that I needed to create two additional methods similar to the new one Wes showed.
Here's the updated version of the getContactNames() function:
func getContactNames()
{
var errorRef: Unmanaged<CFError>?
addressBook = extractABAddressBookRef(ABAddressBookCreateWithOptions(nil, &errorRef))
var contactList: NSArray = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue()
println("records in the array \(contactList.count)")
for record:ABRecordRef in contactList {
var contactPerson: ABRecordRef = record
var contactName: String = ABRecordCopyCompositeName(contactPerson).takeRetainedValue() as NSString
println ("contactName \(contactName)")
var emailArray:ABMultiValueRef = extractABEmailRef(ABRecordCopyValue(contactPerson, kABPersonEmailProperty))!
for (var j = 0; j < ABMultiValueGetCount(emailArray); ++j)
{
var emailAdd = ABMultiValueCopyValueAtIndex(emailArray, j)
var myString = extractABEmailAddress(emailAdd)
println("email: \(myString)")
}
}
}
And here are the two additional functions I created:
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
}
Thanks again to Wes for his help on my initial question which helped me figure the above out.
If you need email additionally to matt's answer:
func getContacts() {
if !self.determineStatus() {
println("not authorized")
}
let people = ABAddressBookCopyArrayOfAllPeople(adbk).takeRetainedValue() as NSArray as [ABRecord]
for person in people {
// Name
let name = ABRecordCopyCompositeName(person).takeRetainedValue()
// Email
let emails: ABMultiValueRef = ABRecordCopyValue(person, kABPersonEmailProperty).takeRetainedValue()
for (var i = 0; i < ABMultiValueGetCount(emails); i++) {
let email: String = ABMultiValueCopyValueAtIndex(emails, i).takeRetainedValue() as String
println("email=\(email)")
}
}
}
This is an old question, but another answer may still be useful: I made an approach to solve the problems with address book in swift here: https://github.com/SocialbitGmbH/SwiftAddressBook
I should mention that there are many wrappers for ABAddressBook out there which can help you avoid issues like the one you asked about entirely. Thus I consider the link an "answer" to the problem (though it is not answering how to fix your code)
To add to the info here, this is my solution pieced together from various places (is there a good Apple site that really describes this, the docs I've found basically provide almost nothing more than what the names of args/members are):
let addrBook = ABAddressBookCreateWithOptions(nil,nil).takeRetainedValue()
let contacts = ABAddressBookCopyArrayOfAllPeople(addrBook).takeRetainedValue() as NSArray as [ABRecordRef]
for contact in contacts {
let fname = ABRecordCopyValue(contact, kABPersonFirstNameProperty).takeRetainedValue() as! NSString
let lname = ABRecordCopyValue(contact, kABPersonLastNameProperty).takeRetainedValue() as! NSString
let name = String(fname) + " " + String(lname)
var image:UIImage? = nil
if ABPersonHasImageData(contact) {
image = UIImage(data: ABPersonCopyImageDataWithFormat(contact, kABPersonImageFormatThumbnail).takeRetainedValue() as NSData)
}
if let emailRefs: ABMultiValueRef = ABRecordCopyValue(contact, kABPersonEmailProperty).takeRetainedValue() {
let nEmailsForContact = ABMultiValueGetCount(emailRefs)
if nEmailsForContact > 0 {
if let emailArray: NSArray = ABMultiValueCopyArrayOfAllValues(emailRefs).takeRetainedValue() as NSArray {
for emailW in emailArray {
let email = String(emailW)
if email.containsString("#") {
let c: EmailContact = EmailContact(n: name, e: email, a: false, i: image)
mEmailContacts.append(c)
}
}
}
}
}
}
Oddly, you have to check to make sure there is an image if you want to access it; and you have to check that there is at least one email for a contact before trying to extract them (why doesn't it just return an empty list instead???).
The 'EmailContact" class is something that I made to capture the results, its not shown but the code snippet does show how to extract the info for the current version of swift/ios.
Also, I note that web site settings seem to come up in the EmailArray for contacts as well as actual emails. For now I just check for an "#" sign to determine if its really an email, but is there a better or 'official' way to do that?
Finally, hopefully this is memory leak safe.
Oh, of course this is done after getting permission, if you are not sure how to do that then this site is good: http://www.raywenderlich.com/63885/address-book-tutorial-in-ios
Other answers provided here were useful, and guided this answer, but had errors and/or were not updated for Swift 3. The following class provides a number of simplifications and safety improvements.
Usage is simply to call AddressBookService.getContactNames
There are good reasons to still need to use the ABAddressBook framework, as CNContact does not provide some key data, including creation and modification dates for instance. The deprecated method warnings are somewhat distracting when working with the code, so this code suppresses the warnings that the ABAddressBook methods were deprecated from iOS 9 onwards, instead providing just a single warning to this effect wherever you call the class below.
//
// AddressBookService.swift
//
import AddressBook
#available(iOS, deprecated: 9.0)
class AddressBookService: NSObject {
class func getContactNames() {
let authorizationStatus = ABAddressBookGetAuthorizationStatus()
switch authorizationStatus {
case .authorized:
retrieveContactNames()
break
case .notDetermined:
print("Requesting Address Book access...")
let addressBook = AddressBookService.addressBook
ABAddressBookRequestAccessWithCompletion(addressBook, {success, error in
if success {
print("Address book access granted")
retrieveContactNames()
}
else {
print("Unable to obtain Address Book access.")
}
})
break
case .restricted, .denied:
print("Address book access denied")
break
}
}
private class func retrieveContactNames() {
let addressBook = ABAddressBookCreate().takeRetainedValue()
let contactList = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue() as NSArray as [ABRecord]
for (index, record) in contactList.enumerated() {
if let contactName = ABRecordCopyCompositeName(record)?.takeRetainedValue() as String? {
print("Contact \(index): \(contactName))")
}
}
}
}
Not the best solution but until I find this work
let records = ABAddressBookCopyArrayOfAllPeople(self.addressBook).takeRetainedValue()
as NSArray as [ABRecord]
sleep(2)
println(records.count);

Resources