Accessing iOS Address Book with Swift: array count of zero - ios
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);
Related
Error upon core data change processing
I am working on a coin collection app, and I have recently started integrating core data into my app. I have a core data database which stores custom CoinCategory objects and each CoinCategory object has an array of Coin objects. This way, my app stores everything in terms of categories. When I integrated the core data, I can add my first category without any errors and delete it without any problems, but when I add in a second coin category, I experience the following error: 2017-06-26 09:55:37.218 CoinCollection[18889:12839563] -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 2017-06-26 09:55:37.219215-0400 CoinCollection[18889:12839563] [error] error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 with userInfo (null) CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 with userInfo (null) Could anyone please advise me on how to fix this? Thank you very much! I am attaching my code below for my viewcontroller and the app delegate that is running the core data. AppDelegate.swift // We customize the app, system-wide import UIKit import CoreData #UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // MARK: - Core Data stack lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "CoinCollection") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() // MARK: - Core Data Saving support func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. //You should not use this function in a shipping application, although it may be useful during development. let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } } And then for my view controller. Notice that I get a Coin object from a sub-viewcontroller that invokes this method, and that we decide whether the Coin object fits into the existing categories. If not, then we add the new Coin. // Controls the table view controller showing the general coins (one per each category) import UIKit import CoreData class CoinTableViewController: UITableViewController, NSFetchedResultsControllerDelegate { //this is an array of all the coins in the collection //each row of this two-dimensional array represents a new category var coinsByCategory: [CoinCategoryMO] = [] var fetchResultController: NSFetchedResultsController<CoinCategoryMO>! //other attributes.... //////////////////////////////////////////////////////////////////////// override func viewDidLoad() { super.viewDidLoad() ////////////////////////////////////////////////////////////////////////////////// //we now fetch the data let fetchRequest : NSFetchRequest<CoinCategoryMO> = CoinCategoryMO.fetchRequest() if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) { let context = appDelegate.persistentContainer.viewContext let sortDescriptor = NSSortDescriptor(key: "coinCategory", ascending: true) fetchRequest.sortDescriptors = [sortDescriptor] fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) fetchResultController.delegate = self do { try fetchResultController.performFetch() if let fetchedObjects = fetchResultController.fetchedObjects { self.coinsByCategory = fetchedObjects } } catch { print(error) } } //configure even more.... } // MARK: - Table view data soure func deleteCoinCategory(rowPath: IndexPath) { if 0 <= rowPath.row && rowPath.row < self.coinsByCategory.count { //we have just tested that the rowPath index is valid if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) { let context = appDelegate.persistentContainer.viewContext let coinCategoryToDelete = self.fetchResultController.object(at: rowPath) context.delete(coinCategoryToDelete) appDelegate.saveContext() } } } func deleteCoin(c: Coin, indexOfSelectedCategory: IndexPath) -> Bool { //we have a coin that we want to delete from this viewcontroller //and the data contained in it. // //the parameter indexOfSelectedCategory refers to the IndexPath of the //row in the TableView contained in THIS viewcontroller whose category //of coins we are modifying in this method // //Return value: a boolean that indicates whether a single coin has //been deleted - meaning that the user should return to the parentviewcontroller if 0 < indexOfSelectedCategory.row && indexOfSelectedCategory.row < self.coinsByCategory.count && self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.hasCoin(c: c) == true { //the index is valid as it refers to a category in the coinsByCategory array //and the examined category has the coin in question if self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.count == 1 { //the coin "c" that we are going to delete is the only coin in the entire category self.deleteCoinCategory(rowPath: indexOfSelectedCategory) return true } else { //more than one coin in the category self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.removeCoin(c: c) //we save the changes in the database... if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) { appDelegate.saveContext() } return false } } return false } func addCoin(coinToAdd: Coin) { //we check over each category to see if the coin can be added for category in self.coinsByCategory { if category.coinCategory?.coinFitsCategory(aCoin: coinToAdd) == true { //we can add the coin to the category category.coinCategory?.addCoin(newCoin: coinToAdd) if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) { //we save changes to the database appDelegate.saveContext() //we are DONE with this function return } } } //since the coinToAdd does not fall in the existing categories, we create a new one if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) { let newCategory = CoinCategoryMO(context: appDelegate.persistentContainer.viewContext) newCategory.coinCategory = CoinCategory(coins: [coinToAdd], categoryType: CoinCategory.CategoryTypes.COUNTRY_VALUE_AND_CURRENCY) print("Saving data to context ...") appDelegate.saveContext() } } //delegate methods control the core data database func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert : if let newIndexPath = newIndexPath { tableView.insertRows(at: [newIndexPath], with: .fade) } case .delete: if let indexPath = indexPath { tableView.deleteRows(at: [indexPath], with: .fade) } case .update: if let indexPath = indexPath { tableView.reloadRows(at: [indexPath], with: .fade) } default: tableView.reloadData() } if let fetchedObjects = controller.fetchedObjects { self.coinsByCategory = fetchedObjects as! [CoinCategoryMO] } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.endUpdates() } I am looking forward to hearing your thoughts and thanks in advance for the help! EDIT: My code for the CoinCategory class: // This class is supposed to represent a category of coin objects in CoreData import Foundation import CoreData public class CoinCategory: NSObject, NSCoding { //These are the various types of categories that a user can create out of their coin collection enum CategoryTypes : NSString { case COUNTRY_VALUE_AND_CURRENCY = //... case COUNTRY = //... case YEAR = //... case CURRENCY = //... case NO_CATEGORY = //... } //this struct is used to encode data in Key-Value pairs per the NSCoding protocol struct Keys { static let Current_Category_Type = "current_category_type" static let Coins_In_Category = "coins_in_category" } //this is the collection of the coins in the category var coinsInCategory: [Coin] = [] //initially we have no coins in the collection var currentCategoryType : CategoryTypes.RawValue = "" public var count : NSNumber { get { //..number of coins in category } } public var array : [Coin] { get { //read-only copy of the Coins array.. } } public required init?(coder aDecoder: NSCoder) { //we decode this object's information if let categoryTypeObject = aDecoder.decodeObject(forKey: Keys.Current_Category_Type) as? CategoryTypes.RawValue { self.currentCategoryType = categoryTypeObject } if let coinsInCategoryArrayObject = aDecoder.decodeObject(forKey: Keys.Coins_In_Category) as? [Coin] { self.coinsInCategory = coinsInCategoryArrayObject } } public func encode(with aCoder: NSCoder) { //we encode this object's information aCoder.encode(currentCategoryType, forKey: Keys.Current_Category_Type) aCoder.encode(self.coinsInCategory, forKey: Keys.Coins_In_Category) } override init() { super.init() self.currentCategoryType = CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue self.coinsInCategory = [] } convenience init(coins: [Coin], categoryType: CategoryTypes.RawValue) { self.init() self.coinsInCategory = coins if isACategoryType(categoryType: categoryType) == true { self.currentCategoryType = categoryType } else { self.currentCategoryType = CategoryTypes.NO_CATEGORY.rawValue } } func isACategoryType(categoryType: NSString) -> Bool { switch categoryType { case CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue: return true case CategoryTypes.COUNTRY.rawValue: return true case CategoryTypes.YEAR.rawValue: return true case CategoryTypes.CURRENCY.rawValue: return true default: return false } } func addCoin(newCoin: Coin) { //we are adding a new Coin object to this category //if it falls into the category's type if self.coinFitsCategory(aCoin: newCoin) == true { self.coinsInCategory.append(newCoin) } } func coinFitsCategory(aCoin: Coin) -> Bool { //this function tests if aCoin fits into the category type //but that all varies depending on which category the coin is if self.coinsInCategory.count == 0 { //this category is currently empty, so any addition goes! return true } //otherwise, this category is not empty... so we are now going to //examine the situation more critically let testCoin = self.coinsInCategory[0] switch self.currentCategoryType { case CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue: return (testCoin.getCountry().lowercased == aCoin.getCountry().lowercased) && (testCoin.getValue() == aCoin.getValue()) && (testCoin.getDenomination().lowercased == aCoin.getDenomination().lowercased) case CategoryTypes.COUNTRY.rawValue: return testCoin.getCountry().lowercased == aCoin.getCountry().lowercased case CategoryTypes.CURRENCY.rawValue: return testCoin.getDenomination().lowercased == aCoin.getDenomination().lowercased case CategoryTypes.YEAR.rawValue: return testCoin.getYear() == aCoin.getYear() default: return false } } func getIndexOfCoinInCollection(coin: Coin) -> Int { //we are going to return -1 if the coin does not exist in the collection //and are going to return the index otherwise if yes for i in 0..<self.coinsInCategory.count { if coinsInCategory[i] == coin { return i } } //have not found anything return -1 } func removeCoin(at: Int) { //we remove the coin at the index if it is in a valid range of the coinInCategory array if isValidArrayIndex(index: at) { self.coinsInCategory.remove(at: at) } } func getCoin(at: Int) -> Coin? { //we return nil if there is an issue in accessing the coin if isValidArrayIndex(index: at) { return self.coinsInCategory[at] } else { return nil } } func assignCoin(at: Int,c: Coin) { if isValidArrayIndex(index: at) { self.coinsInCategory[at].assign(right: c) } } func deleteAllCoins() { //we delete all the coin in this category self.coinsInCategory.removeAll() } func removeCoin(c: Coin) { //we delete a coin from the category for i in 0..<self.coinsInCategory.count { if self.coinsInCategory[i] == c { //the coin at index "i" is equal to the coin "c" that we want to delete from the category self.coinsInCategory.remove(at: i) return } } } func hasCoin(c: Coin) -> Bool { return getIndexOfCoinInCollection(coin: c) != -1 } func swapValuesWithAnotherCategory(other: CoinCategory) { swap(&self.currentCategoryType, &other.currentCategoryType) swap(&self.coinsInCategory,&other.coinsInCategory) } func swapCoins(indexOne: Int, indexTwo: Int) { if isValidArrayIndex(index: indexOne) && isValidArrayIndex(index: indexTwo) { swap(&self.coinsInCategory[indexOne],&self.coinsInCategory[indexTwo]) } } private func isValidArrayIndex(index: Int) -> Bool { return (0 <= index && index < coinsInCategory.count) } } And then the coin category class: // This provides the class definition of Class Coin for the project import UIKit import CoreData enum TimePeriods: String { //this enumeration represents the different time periods that a //coin was minted in, for the sake of this programn case BCE = "BCE" case CE = "CE" } public class Coin : NSObject, NSCoding { //this struct represents all the keys used in encoding and decoding this object struct Keys { static let Country = "country" static let Mint = "mint" static let Year = "year" static let Currency = "currency" static let Value = "value" static let Grade = "grade" static let Comments = "comments" static let NumInstances = "numberInstances" static let Description = "description" static let Obverse = "obverse" static let Reverse = "reverse" } //this represents a coin in the table view static let GRADING_LOWER_LIMIT: NSNumber = 1 static let GRADING_UPPER_LIMIT: NSNumber = 70 //this represents the default strings returned if a field does not have the needed information static let DEFAULT_DESCRIPTIVE_NAME: NSString = "(Description?)" static let DEFAULT_COMMENTS: NSString = "(Comments?)" static let DEFAULT_DENOMINATION: NSString = "(Denomination?)" static let DEFAULT_MINT: NSString = "(Mint?)" static let DEFAULT_COUNTRY: NSString = "(Country?)" static let DEFAULT_YEAR: NSString = "(Year?)" static let DEFAULT_GRADE: NSString = "(Grade?)" static let DEFAULT_VALUE_AND_DENOMINATION: NSString = "(Value?) (Currency?)" static let OBVERSE_IMAGE_STRING : NSString = "Obverse" static let REVERSE_IMAGE_STRING : NSString = "Reverse" static private let BULLET = "➣ " //represents the kind of bullet to be used to build a complete summary of the coin //declare members with setters and getters private var country: NSString = "" //what country/empire/etc. used in? private var mint: NSString = "" //where minted? EX: US Mint, St. Petersburg private var year: NSNumber? = nil //what year minted? per gregorian calendar //the year can be negative to represent the BCE period //positive to represent the CE periods private var typeCurrency: NSString = "" //what is the unit of value? EX: Cents, dollars, centavos, etc private var theValue: NSNumber = 0 //how many? EX: how many dollars, cents, centavos, etc.? //additional information about the coin private var grade: NSNumber? //on the american grading scale for coins. 1-70 private var comments: NSString = "" //extra comments stored by the user for himself private var numberOfInstances: NSNumber = 0 //number of coins exactly like this. EX: 1,2,3,4...etc? For each instance, it must be >= 1. //This describes the type of the coin //EX: Walking Liberty Quarter, Barber Quarter, Standing Liberty Quarter... etc private var descriptiveName: NSString = "" private var obverseImage: UIImage? = nil private var reverseImage: UIImage? = nil public var valueAndDenomination: NSString { get { //need to check four cases //case 1: we have the right values for value and denomination //case 2: we do not have a value but do have denomination //case 3: we have a value but do not have denomination //case 4: we do not have both // //the reason why we consider 0 to be an empty value is because a coin that was worth //nothing would not have been minted in the first place!!! if (self.theValue != 0 && self.typeCurrency != "") { //have value and denomination return "\(self.theValue) \(self.typeCurrency)" as NSString //like "20 Cents" } else if (self.theValue == 0 && self.typeCurrency != "" ) { //do not have value, but have denomination return "(Value?) \(self.typeCurrency)" as NSString } else if (self.theValue != 0 && self.typeCurrency == "") { //we have value, but do not have denomination return "\(self.theValue) (Currency?)" as NSString } else { //we do not have both return Coin.DEFAULT_VALUE_AND_DENOMINATION as NSString } } } public required init?(coder aDecoder: NSCoder) { //we decode this object's information if let countryObject = aDecoder.decodeObject(forKey: Keys.Country) as? NSString { self.country = countryObject } if let mintObject = aDecoder.decodeObject(forKey: Keys.Country) as? NSString { self.mint = mintObject } if let yearObject = aDecoder.decodeObject(forKey: Keys.Year) as? NSNumber { self.year = yearObject } if let currencyObject = aDecoder.decodeObject(forKey: Keys.Currency) as? NSString { self.typeCurrency = currencyObject } if let valueObject = aDecoder.decodeObject(forKey: Keys.Value) as? NSNumber { self.theValue = valueObject } if let gradeObject = aDecoder.decodeObject(forKey: Keys.Grade) as? NSNumber { self.grade = gradeObject } if let commentObject = aDecoder.decodeObject(forKey: Keys.Comments) as? NSString { self.comments = commentObject } if let numInstancesObject = aDecoder.decodeObject(forKey: Keys.NumInstances) as? NSNumber { self.numberOfInstances = numInstancesObject } if let descriptiveNameObject = aDecoder.decodeObject(forKey: Keys.Description) as? NSString { self.descriptiveName = descriptiveNameObject } if let obverseImageObject = aDecoder.decodeObject(forKey: Keys.Obverse) as? UIImage { self.obverseImage = obverseImageObject } if let reverseImageObject = aDecoder.decodeObject(forKey: Keys.Reverse) as? UIImage { self.reverseImage = reverseImageObject } } override init() { //default initializer super.init() self.country = "" self.mint = "" self.year = nil self.typeCurrency = "" self.theValue = 0 self.comments = "" self.numberOfInstances = 1 self.descriptiveName = "" self.obverseImage = nil self.reverseImage = nil } init(country: NSString,year: Int?,typeCurrency: NSString, theValue: NSNumber,mint: NSString,grade: Int?,numInstances: NSNumber = 1,description: NSString, comments: NSString) { super.init() self.country = country self.mint = mint self.year = year! as NSNumber self.typeCurrency = typeCurrency self.theValue = theValue self.comments = comments self.numberOfInstances = numInstances self.descriptiveName = description self.obverseImage = nil self.reverseImage = nil } init(country: NSString,year: NSNumber?,typeCurrency: NSString, theValue: NSNumber,mint: NSString,grade: NSNumber?,numInstances: NSNumber = 1,description: NSString, comments: NSString,obverseImage: UIImage, reverseImage: UIImage) { super.init() self.country = country self.mint = mint self.year = year self.typeCurrency = typeCurrency self.theValue = theValue self.comments = comments self.numberOfInstances = numInstances self.descriptiveName = description } public func encode(with aCoder: NSCoder) { //we encode the coin's information aCoder.encode(self.country, forKey: Keys.Country) aCoder.encode(self.mint, forKey: Keys.Mint) aCoder.encode(self.year, forKey: Keys.Year) aCoder.encode(self.typeCurrency, forKey: Keys.Currency) aCoder.encode(self.theValue, forKey: Keys.Value) aCoder.encode(self.grade, forKey: Keys.Grade) aCoder.encode(self.comments, forKey: Keys.Comments) aCoder.encode(self.numberOfInstances, forKey: Keys.NumInstances) aCoder.encode(self.descriptiveName, forKey: Keys.Description) aCoder.encode(self.obverseImage, forKey: Keys.Obverse) aCoder.encode(self.reverseImage, forKey: Keys.Reverse) } //setter and getter functions for class members... func getCompleteSummary() -> NSString { //returns a bulleted list that represents the coin //and it describes every single detail... } func getIncompleteSummary() -> String { //returns a bulleted list string that represents the coin //and it describes every single detail... } ///////////////////////////////////////////////////////////////////////////////// func ofSameType(rhs: Coin) -> Bool { return (self.getCountry().lowercased == rhs.getCountry().lowercased) && (self.getValue() == rhs.getValue()) && (self.getDenomination().lowercased == rhs.getDenomination().lowercased) } public static func==(lhs: Coin, rhs: Coin) -> Bool { //we compare two coin objects for equality in ALL Categories return lhs.country.lowercased == rhs.country.lowercased && lhs.theValue == rhs.theValue && lhs.typeCurrency.lowercased == rhs.typeCurrency.lowercased && lhs.mint.lowercased == rhs.mint.lowercased && lhs.year == rhs.year && lhs.grade == rhs.grade && lhs.comments == rhs.comments && lhs.numberOfInstances == rhs.numberOfInstances && lhs.descriptiveName == rhs.descriptiveName && lhs.obverseImage == rhs.obverseImage && lhs.reverseImage == rhs.reverseImage } func assign(right: Coin) { //we implement this instead of overloading the assignment "=" operator //as it is not possible to overload the "=" operator //we assign the right-hand-coin's field values //to the left-hand coin's side self.country = right.country self.theValue = right.theValue self.typeCurrency = right.typeCurrency self.mint = right.mint self.year = right.year self.grade = right.grade self.comments = right.comments self.numberOfInstances = right.numberOfInstances self.descriptiveName = right.descriptiveName self.obverseImage = right.obverseImage self.reverseImage = right.reverseImage } }
It looks like the problem is that your CoinCategory class does not implement the compare: method: [CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 My guess is that the FRC is trying to compare the coinCategory attribute of the second CoinCategoryMO to the coinCategory attribute of the first CoinCategoryMO in order to get them in the correct order (since your FRC is sorted using the coinCategory attribute). So the direct answer is that you need to implement a compare: method for your CoinCategory class. But I think this is just an indicator that you need to change your approach. Rather than having an Entity with attributes that are custom objects (or collections thereof), you should probably aim to have multiple Entities (eg. a CoinMO entity, a CoinCategoryMO entity, a CoinCategoryTypeMO entity, etc) with to-many relationships between them. If you show the code for your custom objects (Coin and CoinCategory) it will be easier to advise how best to model them in CoreData.
How to manipulate iOS contacts with SwiftAddressBook?
I'm trying to get all the users contacts and see if they are already singed up in the backend. I'm trying first to get all the contacts using SwiftAddressBook and then I want to edit all the phone numbers and add country code to phone number. I made this function but it doesn't work, it's only showing phone numbers. SwiftAddressBook.requestAccessWithCompletion({ (success, error) -> Void in if success { if let people = swiftAddressBook?.allPeople { for person in people { var phonenumber = String (person.phoneNumbers?.map({$0.value})) print(phonenumber.characters.last) if phonenumber.characters.first == "0" { phonenumber = phonenumber.stringByReplacingCharactersInRange(phonenumber.startIndex..<phonenumber.startIndex.successor(), withString: "+33") } print(phonenumber) } } } else { print("Fail") } })
Well, in first, you should call swiftAddressBook.save() to save the current state of the address book. In second, you are changing a local string variable, which will not affect the address book state. It appears SwiftAddressBook will only make changes to the contacts if you change phoneNumbers array. This should work: SwiftAddressBook.requestAccessWithCompletion({ (success, error) -> Void in if success { if let people = swiftAddressBook?.allPeople { for person in people { if let phoneNumbers = person.phoneNumbers { var phoneNumbersChanged = false var newPhoneNumbers = [MultivalueEntry<String>]() for var phonenumber in phoneNumbers { if phonenumber.value.characters.first == "0" { phonenumber.value = phonenumber.value.stringByReplacingCharactersInRange(phonenumber.value.startIndex..<phonenumber.value.startIndex.successor(), withString: "+33") newPhoneNumbers.append(MultivalueEntry(value: phonenumber.value, label: phonenumber.label, id: phonenumber.id)) phoneNumbersChanged = true } else { newPhoneNumbers.append(phonenumber) } print(phonenumber.value) } if phoneNumbersChanged { person.phoneNumbers = newPhoneNumbers } } } swiftAddressBook.save() } } else { print("Fail") } })
Loop through Realm Object fields in Swift
I have a Realm Object class CoursesModel: Object { dynamic var courseName = "" dynamic var par3Field = 0 dynamic var par4Field = 0 dynamic var par5Field = 0 } When somebody enters the course name I want to check whether it already exists before writing it to Realm. Can you please tell me what I'm doing wrong because it doesn't seem to loop through. class func compareCourse(name : String) -> Bool { let c = name do { let realm = try Realm() let course = realm.objects(CoursesModel) for course in course { if course == c { print("course = \(course)") print("c = \(c)") return true } else { return false } } } catch { // return nil } return false } Any help will be greatly appreciated. EDIT - WORKING CODE HERE class func compareCourse(name : String) -> Bool { let c = name do { let realm = try Realm() let course = realm.objects(CoursesModel) for course in course { let a = course.courseName print("Model Course = \(a)") print("Passed Course = \(c)") if a == c { return true } } } catch { // return nil } return false }
You are returning in both branches of the loop, which immediately exits out of the function. You do not want to return false on the first failure, but only after all have failed (I think).
Getting contact names and phone numbers in Swift
I know this has been asked a lot, yet I'm struggling to find a full solution and the few I find are in Objective C I have managed to get this far public static func refreshContacts(){ let status = ABAddressBookGetAuthorizationStatus() if status == .Denied || status == .Restricted { // user previously denied, to tell them to fix that in settings return } // open it var error: Unmanaged<CFError>? let addressBook: ABAddressBook? = ABAddressBookCreateWithOptions(nil, &error)?.takeRetainedValue() if addressBook == nil { println(error?.takeRetainedValue()) return } // request permission to use it ABAddressBookRequestAccessWithCompletion(addressBook) { granted, error in if !granted { // warn the user that because they just denied permission, this functionality won't work // also let them know that they have to fix this in settings return } if let people = ABAddressBookCopyArrayOfAllPeople(addressBook)?.takeRetainedValue() as? NSArray { for person in people{ var name = //?????? var phoneumber = //?????? } } } } If you read the comment, you can see there is one point where I am not too sure what to do How can i get the name and phone numbers?
I found a solution public static func refreshContacts(){ let status = ABAddressBookGetAuthorizationStatus() if status == .Denied || status == .Restricted { // user previously denied, to tell them to fix that in settings return } // open it var error: Unmanaged<CFError>? let addressBook: ABAddressBook? = ABAddressBookCreateWithOptions(nil, &error)?.takeRetainedValue() if addressBook == nil { println(error?.takeRetainedValue()) return } // request permission to use it ABAddressBookRequestAccessWithCompletion(addressBook) { granted, error in if !granted { // warn the user that because they just denied permission, this functionality won't work // also let them know that they have to fix this in settings return } if let people = ABAddressBookCopyArrayOfAllPeople(addressBook)?.takeRetainedValue() as? NSArray { for person in people{ if let name = ABRecordCopyValue(person, kABPersonFirstNameProperty).takeRetainedValue() as? String { println(name)//persons name } let numbers:ABMultiValue = ABRecordCopyValue( person, kABPersonPhoneProperty).takeRetainedValue() for ix in 0 ..< ABMultiValueGetCount(numbers) { let label = ABMultiValueCopyLabelAtIndex(numbers,ix).takeRetainedValue() as String let value = ABMultiValueCopyValueAtIndex(numbers,ix).takeRetainedValue() as! String println("Phonenumber \(label) is \(value)) } } } } } adapted from https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch18p713addressBook/ch31p973addressBook/ViewController.swift
Loading a UITableView with an array from ABAddressBook (Swift)
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.