I am creating an onboarding portion of an app which gets the user's contacts to check which already have the app to add as friends. I'm using the CNContact framework. I have created several methods I'm using to get a full list of the users' contacts, check if they have the app, and enumerate them in a UITableView. However, when the view loads, the app crashes with the error "A property was not requested when contact was fetched." I already make a fetch request with the keys CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, ad CNContactImageDataKey. I've included all my code here:
import Foundation
import Contacts
import PhoneNumberKit
struct ContactService {
static func createContactArray() -> [CNContact] {
var tempContacts = [CNContact]()
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let _ = error {
print("failed to request access to contacts")
return
}
if granted {
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactImageDataKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
request.sortOrder = CNContactSortOrder.familyName
do {
try store.enumerateContacts(with: request, usingBlock: { (contact, stop) in
tempContacts.append(contact)
})
} catch {
print("unable to fetch contacts")
}
print("created contact list")
}
}
return tempContacts
}
static func createFetchedContactArray(contactArray: [CNContact], completion: #escaping ([FetchedContact]?) -> Void) -> Void {
var temp = [FetchedContact]()
getNumsInFirestore { (nums) in
if let nums = nums {
for c in contactArray {
let f = FetchedContact(cnContact: c, numsInFirebase: nums)
temp.append(f)
}
return completion(temp)
} else {
print("Error retrieving numbers")
}
}
return completion(nil)
}
static func getNumsInFirestore(_ completion: #escaping (_ nums : [String]?) -> Void ) -> Void {
var numsInFirebase = [String]()
let db = FirestoreService.db
db.collection(Constants.Firestore.Collections.users).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting user documents: \(err)")
completion(nil)
} else {
for doc in querySnapshot!.documents {
let dict = doc.data()
numsInFirebase.append(dict[Constants.Firestore.Keys.phone] as! String)
}
completion(numsInFirebase)
}
}
}
static func getPhoneStrings(contact: CNContact) -> [String] {
var temp = [String]()
let cnPhoneNums = contact.phoneNumbers
for n in cnPhoneNums {
temp.append(n.value.stringValue)
}
return temp
}
static func hasBump(contact: CNContact, completion: #escaping (_ h: Bool) -> Void ) -> Void {
let contactNums = ContactService.getPhoneStrings(contact: contact)
ContactService.getNumsInFirestore { (nums) in
if let nums = nums {
return completion(contactNums.contains(where: nums.contains))
} else {
print("Error retrieving numbers from firestore")
return completion(false)
}
}
}
static func anyContactsWithBump(completion: #escaping (_ yes: Bool) -> Void) {
let contacts = createContactArray()
var tempBool = false
let workgroup = DispatchGroup()
for c in contacts {
workgroup.enter()
hasBump(contact: c) { (has) in
if has {
tempBool = true
}
workgroup.leave()
}
}
workgroup.notify(queue: .main) {
completion(tempBool)
}
}
}
Then I call the methods in the view controller class:
import UIKit
import Contacts
import FirebaseDatabase
import Firebase
import FirebaseFirestore
class AddContactsVC: UIViewController {
var fetchedContactsWithBump: [FetchedContact] = []
override func viewDidLoad() {
let cnContacts = ContactService.createContactArray()
var contactsWithBump = [CNContact]()
let workgroup = DispatchGroup()
for contact in cnContacts {
workgroup.enter()
ContactService.hasBump(contact: contact) { (has) in
if has {
contactsWithBump.append(contact)
}
workgroup.leave()
}
}
workgroup.notify(queue: .main) {
print("Number of contacts with Bump: \(contactsWithBump.count)")
ContactService.createFetchedContactArray(contactArray: contactsWithBump) { (fetchedContacts) in
if let fetchedContacts = fetchedContacts {
self.fetchedContactsWithBump = fetchedContacts
} else {
print("Error creating fetchedContacts array.")
}
}
}
I also get the message "Error creating fetchedContacts array" in the console, so I know something is going wrong with that method, I'm just not sure what. Any help is appreciated!
Edit: The exception is thrown at 3 points: 1 at the first line of my FetchedContact init method, which looks like this:
init(cnContact: CNContact, numsInFirebase: [String]) {
if cnContact.imageDataAvailable { self.image = UIImage(data: cnContact.imageData!) }
It also points to the line let f = FetchedContact(cnContact: c, numsInFirebase: nums) in createFetched contact array, and finally at my completion(numsInFirebase) call in getNumsInFirestore.
To start with
let contacts = createContactArray()
will always return an empty array.
This function has a return statement outside the closure so will immediately return an empty array.
Change createContactArray to use a completion handler like the other functions you have to populate contacts from inside the closure.
Related
I am making and expanding an app for lists and adding extra data as I go. I have added a bool to mark the item as completed.
I want to change the value of a bool stored in core data. I can add and delete orders but now I'm looking to change properties. Please can I have help on the best way to change these?
I have made a func changeCompleated, but can't work out how to get it to work in my core data manager.
class CoreDataManager {
static let shared = CoreDataManager(moc: NSManagedObjectContext.current)
var moc: NSManagedObjectContext
private init(moc: NSManagedObjectContext) {
self.moc = moc
}
private func fetchOrder(name: String) -> Order? {
var orders = [Order]()
let request: NSFetchRequest<Order> = Order.fetchRequest()
request.predicate = NSPredicate(format: "name == %#", name)
do {
orders = try self.moc.fetch(request)
} catch let error as NSError {
print(error)
}
return orders.first
}
func changeCompleated(name:String, completed: Bool) {
do {
if let order = fetchOrder(name: name) {
self.moc.perform {
}
try self.moc.save()
}
} catch let error as NSError {
print(error)
}
}
func deleteOrder(name: String) {
do {
if let order = fetchOrder(name: name) {
self.moc.delete(order)
try self.moc.save()
}
} catch let error as NSError {
print(error)
}
}
func getAllOrders() -> [Order] {
var orders = [Order]()
let orderRequest: NSFetchRequest<Order> = Order.fetchRequest()
do {
orders = try self.moc.fetch(orderRequest)
} catch let error as NSError {
print(error)
}
return orders
}
func saveOrder(id: UUID, name: String, type: String, qty: Double, urgent: Bool, complete: Bool) {
let order = Order(context: self.moc)
order.id = id
order.name = name
order.type = type
order.qty = qty
order.urgent = urgent
order.complete = complete
do {
try self.moc.save()
} catch let error as NSError {
print(error)
}
}
}
Almost, just add a line to change the value, the perform block is not needed
func changeCompleated(name: String, completed: Bool) {
guard let order = fetchOrder(name: name) else { return }
do {
order.complete = completed
try self.moc.save()
} catch {
print(error)
}
}
And you can also shorten fetchOrder
private func fetchOrder(name: String) -> Order? {
let request: NSFetchRequest<Order> = Order.fetchRequest()
request.predicate = NSPredicate(format: "name == %#", name)
do {
return try self.moc.fetch(request).first
} catch {
print(error)
}
}
I am developing an application in which I need to fetch all the contacts from device and then set it to favorite contact on button press. I am able to fetch all contacts using [CNContact] in iOS 9 and 10. But don't know how to set it as a favorite contact.
Can we set CNContact as a favorite contact?
Can we make changes in CNContact?
You can store Favourites to Realm DB. Like This,
class FavouriteList: Object {
let favouriteList : List<FavouriteContact> = List<FavouriteContact>()
}
class FavouriteContact: Object {
dynamic var identifier : String? = ""
override class func primaryKey() -> String? {
return "identifier"
}
}
// Add Favourite Contact in Realm
class func add(identifier: String) -> Bool {
var realm: Realm!
do {
realm = try Realm()
realm.beginWrite()
} catch {
print(error.localizedDescription)
}
let realmTask: FavouriteList= FavouriteList()
let favContact: FavouriteContact = FavouriteContact()
// Check ID Exist or Not
let idExists: FavouriteContact? = realm.object(ofType: FavouriteContact.self, forPrimaryKey: identifier)
if idExists?.identifier != nil {
realm.cancelWrite()
return false
} else {
favContact.identifier = identifier
realmTask.favouriteList.append(favContact)
realm.add(realmTask)
}
// Realm Commit
do {
try realm.commitWrite()
} catch {
print("Realm Task Write Error : ", error.localizedDescription)
}
return true
}
// Remove Favourite Contact
class func remove(identifier: String) -> Bool {
var realm: Realm!
do {
realm = try Realm()
realm.beginWrite()
} catch {
print(error.localizedDescription)
}
// Check ID Exist or Not
let idExists: FavouriteContact? = realm.object(ofType: FavouriteContact.self, forPrimaryKey: identifier)
if idExists?.identifier != nil {
realm.delete(idExists!)
} else {
realm.cancelWrite()
return false
}
// Realm Commit
do {
try realm.commitWrite()
} catch {
print("Realm Task Write Error : ", error.localizedDescription)
}
return true
}
// Get Favourite List
class func get(completionHandler: #escaping (_ result: [CNContact]) -> ()) {
var favourites: [CNContact] = [CNContact]()
do {
let realm = try Realm()
let dataRealmContacts: Results<FavouriteList> = realm.objects(FavouriteList.self)
for item in dataRealmContacts {
for contactID in item.favouriteList {
if contactID.identifier != nil {
favourites.append(getContactFromID(identifier: contactID.identifier!))
}
}
}
completionHandler(favourites)
} catch {
print(error.localizedDescription)
completionHandler(favourites)
}
}
Trying unsuccessfully to get a contact match providing contact Identifier. I desire to return contact, then to use the image associated with it. I am getting a nil match. thank you. This code I got from a demo, I'm kinda new to programming
import Contacts
var contact = CNContact()
var contactStore = CNContactStore()
let foundContact = getContactFromID("94AAD3B1-E9E1-48C9-A796-F7EC1014230A")
func getContactFromID(contactID: String) -> CNContact {
AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
if accessGranted {
let predicate = CNContact.predicateForContactsWithIdentifiers([contactID])
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactPhoneNumbersKey, CNContactImageDataKey, CNContactImageDataAvailableKey]
var contacts = [CNContact]()
var message: String!
let contactsStore = AppDelegate.getAppDelegate().contactStore
do {
contacts = try contactsStore.unifiedContactsMatchingPredicate(predicate, keysToFetch: keys)
if contacts.count == 0 {
message = "No contacts were found matching the given name."
}
}
catch {
message = "Unable to fetch contacts."
}
if message != nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
Utility.showAlert(nil, message: message)
})
} else {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.contact = contacts[0]
print("self.contact: \(self.contact)")
})
}
}
}
return self.contact
}
I solved it :), I removed the dispatch_async stuff, works now: here is fixed code.
func getContactFromID(contactID: String) -> CNContact {
AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
if accessGranted {
let predicate = CNContact.predicateForContactsWithIdentifiers([contactID])
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactPhoneNumbersKey, CNContactImageDataKey, CNContactImageDataAvailableKey]
var contacts = [CNContact]()
var message: String!
let contactsStore = AppDelegate.getAppDelegate().contactStore
do {
contacts = try contactsStore.unifiedContactsMatchingPredicate(predicate, keysToFetch: keys)
if contacts.count == 0 {
message = "No contacts were found matching the given name."
}
}
catch {
message = "Unable to fetch contacts."
}
self.contact = contacts[0]
}
}
return self.contact
}
I've just learned how to create Watch apps from a Watch OS 1 tutorial and most of it translates to OS 2 except for communication from the Watch and iPhone. Here are the steps I'm taking:
1 - Watch InterfaceController.swift - create a WCSession, sendMessage to iPhone if user is logged in or not. If yes ask for table data, if no show a label to go to phone.
2 - I correctly get whether user is logged in or not, then I ask for an array of dog names to populate my table.
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
if (WCSession.isSupported()) {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
print("Activated Session ----")
if session.reachable == true {
session.sendMessage(["content" : "isLoggedIn"], replyHandler: { (result) -> Void in
if let response = result["isLoggedIn"] as? String {
if response == "true" {
userLoggedOn = true
self.session.sendMessage(["content":"getDogNames"], replyHandler: { (result) -> Void in
if let nameRequest = result as? [String:[String]] {
if let dogData = nameRequest["names"] {
if dogData.count > 0 {
dogNames = dogData
self.logInLabel.setHidden(true)
self.loadTableData()
}
else {
self.logInLabel.setHidden(false)
self.logInLabel.setText("Add dog profiles from iPhone")
}
print(dogData)
}
}
}, errorHandler: { (error) -> Void in
print("got names error")
})
} else {
userLoggedOn = false
self.logInLabel.setText("Create a profile from iPhone")
}
}
}, errorHandler: { (error) -> Void in
print("error:------ /(error)")
})
}
}
}
3 - iPhone AppDelegate.swift - In my result, I'm only getting the first object of an array queried from my parse.com database.
#available(iOS 9.0, *)
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
if let watchMessage = message as? [String : String] {
if let content = watchMessage["content"] {
if content == "isLoggedIn" {
if PFUser.currentUser() != nil {
replyHandler(["isLoggedIn":"true"])
}
else {
replyHandler(["isLoggedIn":"false"])
}
} else if content == "getDogNames" {
let query = PFQuery(className: "dogInfo")
query.whereKey("userId", equalTo: (PFUser.currentUser()?.objectId)!)
query.orderByAscending("dogName")
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if error == nil && objects!.count > 0 {
for object in objects! {
var dogsArray = [String]()
if let message = object["dogName"] as? String {
dogsArray.append(message)
}
replyHandler(["names":dogsArray])
}
}
})
}
}
}
}
I've tested this by manually putting in
var dogsArray = ["dog1","dog2","dog3"]
and it results in all 3 items coming back. I know for a fact there are 7 items that I should be getting but only getting the first one. Very strange, any help is much appreciated!
There seems to be a logical error in your code, try changing it to this:
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if error == nil && objects!.count > 0 {
var dogsArray = [String]() // moved this
for object in objects! {
if let message = object["dogName"] as? String {
dogsArray.append(message)
}
}
replyHandler(["names":dogsArray]) // and this
}
})
where I've moved the call to the replyHandler and the creation of the dogsArray outside of the for loop so that it accumulates all the dog names, and then afterwards calls the replyHandler.
Im working with NSURLSession. I have an array with restaurants and i'm requesting the dishes for every restaurant in the array to the api. The dataTask works,i'm just having a real hard time trying to call a method only when the all dataTasks are finished.
self.findAllDishesOfRestaurants(self.restaurantsNearMe) { (result) -> Void in
if result.count != 0 {
self.updateDataSourceAndReloadTableView(result, term: "protein")
} else {
print("not ready yet")
}
}
the self.updateDataSourceAndREloadTableView never gets called, regardless of my completion block. Here is my findAllDishesOfRestaurants function
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
self.allDishes.addObjectsFromArray(dishesArray as [AnyObject])
print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray)
})
completion(result:allDishesArray)
}
}
}
And here is my the function where i perform the dataTasks.
func getDishesByRestaurantName(restaurant:Resturant, completion:(result:NSArray) ->Void) {
var restaurantNameFormatted = String()
if let name = restaurant.name {
for charachter in name.characters {
var newString = String()
var sameCharacter:Character!
if charachter == " " {
newString = "%20"
restaurantNameFormatted = restaurantNameFormatted + newString
} else {
sameCharacter = charachter
restaurantNameFormatted.append(sameCharacter)
}
// print(restaurantNameFormatted)
}
}
var urlString:String!
//not to myself, when using string with format, we need to igone all the % marks arent ours to replace with a string, otherwise they will be expecting to be replaced by a value
urlString = String(format:"https://api.nutritionix.com/v1_1/search/%#?results=0%%3A20&cal_min=0&cal_max=50000&fields=*&appId=XXXXXXXXXappKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXX",restaurantNameFormatted)
let URL = NSURL(string:urlString)
let restaurantDishesArray = NSMutableArray()
let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithURL(URL!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
do {
let anyObjectFromResponse:AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
if let asNSDictionary = anyObjectFromResponse as? NSDictionary {
let hitsArray = asNSDictionary.valueForKey("hits") as? [AnyObject]
for newDictionary in hitsArray! as! [NSDictionary]{
let fieldsDictionary = newDictionary.valueForKey("fields") as? NSDictionary
let newDish = Dish.init(dictionary:fieldsDictionary!, restaurant: restaurant)
restaurantDishesArray.addObject(newDish)
}
}
completion(result:restaurantDishesArray)
} catch let error as NSError {
print("failed to connec to api")
print(error.localizedDescription)
}
}
dataTask.resume()
}
Like i said before, I need to wait until the fun findAllDishesOfRestaurants is done. I tried writing my completion blocks but I'm not sure I'm doing it right. Any help is greatly appreciated. Thank
The problem is that you are calling the completion method in findAllDishesOfRestaurants before al tasks are complete. In fact, you are calling it once for each restaurant in the list, which is probably not what you want.
My recommendation would be for you to look into NSOperationQueue for two reasons:
It will let you limit the number of concurrent requests to the server, so your server does not get flooded with requests.
It will let you easily control when all operations are complete.
However, if you are looking for a quick fix, what you need is to use GCD groups dispatch_group_create, dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify as follows.
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let group = dispatch_group_create() // Create GCD group
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
dispatch_group_enter(group) // Enter group for this restaurant
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
// self.allDishes.addObjectsFromArray(dishesArray as [AnyObject]) <-- do not do this
// print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray) <-- No need for this, remove
dispatch_group_leave(group) // Leave group, marking this restaurant as complete
})
// completion(result:allDishesArray) <-- Do not call here either
}
}
// Wait for all groups to complete
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(result:allDishesArray)
}
}