How to access iOS Contacts in batches? - ios

I'm trying to implement a contacts reader in Xamarin.iOS which tries to iterate over the iOS contacts in all CNContactStore containers. Instead of loading all contacts into memory, I need to iterate over a contacts resultset batch by batch (paging contacts). However all the examples that I saw in SO load almost all contacts into memory first.
i.e. This question has loads of similar examples that read all contacts at once. Although these examples have logic which iterates one by one, it is not evident to me how to skip N and take the next N number of contacts without iterating from the beginning on the next call (which looks sub optimal at least to me).
Apple's own documentation reads
When fetching all contacts and caching the results, first fetch all contacts identifiers, then fetch batches of detailed contacts by identifiers as required
I was able to do this easily for Android using the cursor based approach available in its SDK. Is this at all possible for iOS? If not how can we handle a large number of contacts (e.g. something above 2000, etc.). I don't mind examples in swift. I should be able to convert them to Xamarin.
Thanks in advance.

Here's the approach I took, granted my requirements did not allow persisting contacts, only holding in active memory. Not saying it's the right approach, but fetching all identifiers first, then lazily fetching all keys for a specific contact as needed, did improve performance. It also avoids performing a lookup when the contact doesn't exist.
I also tried using NSCache instead of dictionary, but ran into issue when I needed to iterate over the cache.
I truncated functions that aren't relevant to the topic but hopefully still convey the approach.
import Contacts
extension CNContactStore {
// Used to seed a Contact Cache with all identifiers
func getAllIdentifiers() -> [String: CNContact]{
// keys to fetch from store
let minimumKeys: [CNKeyDescriptor] = [
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactIdentifierKey as CNKeyDescriptor
]
// contact request
let request = CNContactFetchRequest(keysToFetch: minimumKeys)
// dictionary to hold results, phone number as key
var results: [String: CNContact] = [:]
do {
try enumerateContacts(with: request) { contact, stop in
for phone in contact.phoneNumbers {
let phoneNumberString = phone.value.stringValue
results[phoneNumberString] = contact
}
}
} catch let enumerateError {
print(enumerateError.localizedDescription)
}
return results
}
// retreive a contact using an identifier
// fetch keys lists any CNContact Keys you need
func get(withIdentifier identifier: String, keysToFetch: [CNKeyDescriptor]) -> CNContact? {
var result: CNContact?
do {
result = try unifiedContact(withIdentifier: identifier, keysToFetch: keysToFetch)
} catch {
print(error)
}
return result
}
}
final class ContactsCache {
static let shared = ContactsCache()
private var cache : [String : ContactCacheItem] = [:]
init() {
self.initializeCache() // calls CNContactStore().getAllIdentifiers() and loads into cache
NotificationCenter.default.addObserver(self, selector: #selector(contactsAppUpdated), name: .CNContactStoreDidChange, object: nil)
}
private func initializeCache() {
DispatchQueue.global(qos: .background).async {
let seed = CNContactStore.getAllIdentifiers()
for (number, contact) in seed{
let item = ContactCacheItem.init(contact: contact, phoneNumber: number )
self.cache[number] = item
}
}
}
// if the contact is in cache, return immediately, else fetch and execute completion when finished. This is bit wonky to both return value and execute completion, but goal was to reduce visible cell async update as much as possible
public func contact(for phoneNumber: String, completion: #escaping (CNContact?) -> Void) -> CNContact?{
if !initialized { // the cache has not finished seeding, queue request
queueRequest(phoneNumber: phoneNumber, completion: completion) // save request to be executed as soon as seeding completes
return nil
}
// item is in cache
if let existingItem = getCachedContact(for: phoneNumber) {
// is it being looked up
if existingItem.lookupInProgress(){
existingItem.addCompletion(completion: completion)
}
// is it stale or has it never been looked up
else if existingItem.shouldPerformLookup(){
existingItem.addCompletion(completion: completion)
refreshCacheItem( existingItem )
}
// its current, return it
return existingItem.contact
}
// item is not in cache
completion(nil)
return nil
}
private func getCachedContact(for number: String) -> ContactCacheItem? {
return self.cache.first(where: { (key, _) in key.contains( number) })?.value
}
// during the async initialize/seeding of the cache, requests may come in from app, so they are temporarily 'queued'
private func queueRequest(phoneNumber: String, completion: #escaping (CNContact?) -> Void){..}
// upon async initialize/seeding completion, queued requests can be executed
private func executeQueuedRequests() {..}
// if app receives notification of update to user contacts, refresh cache
#objc func contactsAppUpdated(_ notification: Notification) {..}
// if a contact has gone stale or never been fetched, perform the fetch
private func refreshCacheItem(_ item: ContactCacheItem){..}
// if app receives memory warning, dump data
func clearCaches() {..}
}
class ContactCacheItem : NSObject {
var contact: CNContact? = nil
var lookupAttempted : Date? // used to determine when last lookup started
var lookupCompleted : Date? // used to determien when last successful looup completed
var phoneNumber: String //the number used to look this item up
private var callBacks = ContactLookupCompletion() //used to keep completion blocks for lookups in progress, in case multilpe callers want the same contact info
init(contact: CNContact?, phoneNumber: String){..}
func updateContact(contact: CNContact?){..} // when a contact is fetched from store, update it here
func lookupInProgress() -> Bool {..}
func shouldPerformLookup() -> Bool {..}
func hasCallBacks() -> Bool {..}
func addCompletion(completion: #escaping (CNContact?) -> Void){..}
}

Related

Swift 4 Transfer results to main thread when using external networking framework

I wanted to fetch data from the server api.
The issues is that all networking frameworks are doing it Async.
So I have issues that return variable return empty Here is my code.
The view controller where I call the function
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url = "http://api.musixmatch.com/ws/1.1/track.lyrics.get?track_id=12693365&apikey=63ee7da5e2ee269067ecc42b25590922"
let musixrequest = MusicMatchRequest()
let endResults = musixrequest.gettingLyrics(url: url)
if !endResults.isEmpty{
print("The end results are \(endResults)")
}else{
print("No results found")
}
}
Here is my class where I am trying to fetch the data
public class MusicMatchRequest : NSObject{
public override init(){}
public func gettingLyrics(url : String) -> String {
var endResults = ""
DefaultProvider.request(Route(path:"\(url)")).responseJSON { (response:Response<Any>) in
switch response.result{
case .success(let json):
endResults = String(describing:json)
print(endResults)
case .failure(let error):
print("error: \(error)")
}
}
return endResults
}
}
When I am printing the endRsults from the task it is working It print the results but the var endResults return empty.
Idea how to transfer the data .
I have tried two frameworks
Alamofire
Nikka
In both frameworks it's acting the same .
Solution
I don't exactly know what happens under the hood, but as ANY network operation this also has to be asynchronous (meaning it will take a certain amount of time to fetch the data).
let endResults = musixrequest.gettingLyrics(url: url)
If it's synchronously done on the Main thread, it will block it so the user can't interact with the app, which is pretty bad. Given it's asynchronous in your code you read the value in the very next line immediately, here:
if !endResults.isEmpty {
print("The end results are \(endResults)")
} else {
print("No results found")
}
It's very unlikely that the network operation will finish in one line step time, so you won't have the data there.
What you should do is to pass a completion handler in this method:
public func gettingLyrics(url : String) -> String
and dispatch to main thread like this:
DispatchQueue.main.async {
// do you UI stuff here
}
Change you function to this:
public func gettingLyrics(url : String, completionHandler: (String) -> Void)
and call the completion handler in the success branch:
completionHandler(String(describing:json))

Loading data from API pattern issue

I am building an app that populates data in a collectionView. The data come from API calls. When the screen first loads I get the products and store them locally in my ViewController.
My question is when should I get the products again and how to handle screen changing. My data will change when the app is running (sensitive attributes like prices) , but I don't find ideal solution to make the API call each time viewWillAppear is being called.
Can anybody please tell me what is the best pattern to handle this situation. My first though was to check if [CustomObject].isEmpty on viewWillAppear and then make the call. Including a timer that check again every 10-15 minutes for example.
Thank you for your input.
I'm not sure what the data looks like and how your API in detail works, but you certainly don't have to call viewWillAppear when your API updates the data.
There are two possible solutions to be notified when your data is updated.
You can either use a notification that lets you know whether the API is providing some data. After the Data has been provided your notification then calls to update the collection view. You can also include in the objects or structs that contain the data from your API the "didSet" call. Every time the object or struct is being updated the didSet routine is called to update your collection view.
To update your collection view you simply call the method reloadData() and the collection view will update itself and query the data source that now contains the newly received data from your API.
Hope this helps.
There is no set pattern but it is advisable not to send repeated network requests to increase energy efficiency (link). You can check the time interval in ViewWillApear and send the network requests after certain gap or can use timer to send requests at time intervals. First method would be better as it sends request only when user is on that screen. You can try following code snippet to get the idea
class ViewController: UIViewController {
let time = "startTime"
let collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
update()
}
private func update() {
if let startDateTime = NSUserDefaults.standardUserDefaults().objectForKey(time) as? NSDate {
let interval = NSDate().timeIntervalSinceDate(startDateTime)
let elapsedTime = Int(interval)
if elapsedTime >= 3600 {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(startDateTime, forKey: time)
}
} else {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(NSDate(), forKey: time)
}
}
func makeNetworkRequest() {
//Network Request to fetch data and update collectionView
let urlPath = "http://MyServer.com/api/data.json"
guard let endpoint = NSURL(string: urlPath) else {
print("Error creating endpoint")
return
}
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
do {
guard let data = data else {
return
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else {
print("Error in json parsing")
return
}
self.collectionView.reloadData()
} catch let error as NSError {
print(error.debugDescription)
}
}.resume()
}

Firebase downloads same data

I use firebase in swift, I call the listener in viewDidAppear: and as it is written in the docs it should't download the same data again but now every time the view appears the same data will be displayed.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
getData()
self.messagesRef.keepSynced(true)
}
func getData(){
messagesRef.observeEventType(.ChildAdded) { (mesData: FDataSnapshot!) -> Void in
let messageDict = mesData.value as! NSDictionary
let key = mesData.key
self.likedRef = self.ref.childByAppendingPath("likedMessages/\(currentUser.uid)/\(key)")
self.likedRef.observeSingleEventOfType(.Value) { (likedFd: FDataSnapshot!) -> Void in
if likedFd.value is NSNull{
let liked = false
let message = Message(message: messageDict, key: key, liked: liked, topicKey: self.topic.key)
self.messages.append(message)
self.messages.sortInPlace{$0.createdAt.compare($1.createdAt) == .OrderedDescending}
}
else{
let liked = likedFd.value as! Bool
let message = Message(message: messageDict, key: key, liked: liked, topicKey: self.topic.key)
self.messages.append(message)
self.messages.sortInPlace{$0.createdAt.compare($1.createdAt) == .OrderedDescending}
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.messagesTableView.reloadData()
}
self.likedRef.keepSynced(true)
}
print(mesData.value)
}
}
If you add the first listener to a location, Firebase will download the current data for that location and start synchronizing the changes. It will keep a copy of the data available in memory.
If you add a second listener to the location, while the first one is still active, Firebase will not re-download the data. I just quickly verified this http://jsbin.com/foduxun/edit?js,console.
If you remove the first listener before you attach the second listener, Firebase will purge its cache. So then when you attach a listener again, it will re-download the data.
If you want to keep the data available on the device even after you detached all listeners, enable disk persistence:
[Firebase defaultConfig].persistenceEnabled = YES;

Background API POST that handle failure

In my iOS app users complete transactions which I need to post back to the server. I've created a function to do this:
static let configurationParam = NSURLSessionConfiguration.defaultSessionConfiguration()
static var manager = Alamofire.Manager(configuration: configurationParam)
func postItemToServer(itemToPost:DemoItem) {
let webServiceCallUrl = "..."
var itemApiModel:[String: AnyObject] = [
"ItemId": 123,
"ItemName": itemToPost.Name!,
//...
]
ApiManager.manager.request(.POST, webServiceCallUrl, parameters: itemApiModel, encoding: .JSON)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
print("post success")
case .Failure:
print("SERVER RESPONSE: \(response.response?.statusCode)")
}
}
}
Currently I call this once a transaction is complete:
//...
if(transactionCompleted!) {
let apiManager = ApiManager()
apiManager.postItemToServer(self.item)
self.senderViewController!.performSegueWithIdentifier("TransactionCompletedSegue", sender: self)
}
//...
Where DemoItem is a CoreData object.
This all works as expected. However I need the ability to retry the POST request if it fails. For example if the network connection is down at the point of trying post to the server I need to automatically post the data once it becomes active again - at which point there may be several DemoItem's which need to be synced.
I'm new to Swift. In a similar Xamarin app I had a status column in my SQLite database which I set to 'AwaitingSync'. I then had an async timer that ran every 30 seconds, queried the DB for any items which had status='AwaitingSync' and then tried to post them if they existed. If it succeed it updated the status in the DB. I could implement something along the same lines here - but I was never really happy with that implementation as I had a DB query every 30 seconds even if nothing had changed.
Finally, it needs to be still work if the app is terminated. For example any items which weren't synced before the app is killed should sync once the app is resumed. What's the best way to approach this?
Edit
Based on Tom's answer I've created the following:
class SyncHelper {
let serialQueue = dispatch_queue_create("com.mycompany.syncqueue", DISPATCH_QUEUE_SERIAL)
let managedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
func StartSync() {
//Run on serial queue so it can't be called twice at once
dispatch_async(serialQueue, {
//See if there are any items pending to sync
if let itemsToSync = self.GetItemsToSync() {
//Sync all pending items
for itemToSync in itemsToSync {
self.SyncItemToServer(itemToSync)
}
}
})
}
private func GetItemsToSync() -> [DemoItem]? {
var result:[DemoItem]?
do {
let fetchRequest = NSFetchRequest(entityName: "DemoItem")
fetchRequest.predicate = NSPredicate(format: "awaitingSync = true", argumentArray: nil)
result = try managedContext.executeFetchRequest(fetchRequest) as? [DemoItem]
} catch {
//Handle error...
}
return result
}
private func SyncItemToServer(itemToSync:DemoItem) {
let apiManager = ApiManager()
//Try to post to the server
apiManager.postItemToServer(itemToSync:DemoItem, completionHandler: { (error) -> Void in
if let _ = error {
//An error has occurred - nothing need to happen as it will be picked up when the network is restored
print("Sync failed")
} else {
print("Sync success")
itemToSync.awaitingSync = false
do {
try self.managedContext.save()
} catch {
//Handle error...
}
}
})
}
}
I then call this when ever a transaction is completed:
//...
if(transactionCompleted!) {
let syncHelper = SyncHelper()
syncHelper.StartSync()
}
//...
And then finally I've used Reachability.swift to start the sync every time the network connection resumes:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reachability:Reachability?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//...
//Setup the sync for when the network connection resumes
do {
reachability = try Reachability.reachabilityForInternetConnection()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "reachabilityChanged:",
name: ReachabilityChangedNotification,
object: reachability)
try reachability!.startNotifier()
} catch {
print("Unable to create Reachability")
}
return true
}
func reachabilityChanged(note: NSNotification) {
let reachability = note.object as! Reachability
if reachability.isReachable() {
print("Network reachable")
let syncHelper = SyncHelper()
syncHelper.StartSync()
} else {
print("Not reachable")
}
}
}
This all seems to be working. Is this approach ok and have I missed anything which would improve it? The only gap I can see is if the network connectivity is active however the server throws an error for some reason - I guess I could then add a button for the user to retry any pending items.
Firstly, if your concern is whether the network connection is working, you shouldn't be polling at intervals. You should be using iOS's network reachability API to get notified when the network status changes. Apple provides a simple implementation of this and there are numerous alternative implementations online.
Since a sync status value should be a boolean flag, it's not as if a fetch request is a heavy-duty operation, especially if you use reachability. Not only should the fetch request be fast, you can update the flag after the fact in a single step-- use NSBatchUpdateRequest to set the flag to false on every instance you just sent to the server.
If you want to get the sync status out of the persistent store (not a bad idea since it's metadata), you'll need to maintain your own list of unsynced objects. The best way to do this is by tracking the objectID of the managed objects awaiting sync. That would be something like:
Get the objectID of a newly changed managed object
Convert that to an NSURL using NSManagedObjectID's URIRepresentation() method.
Put the NSURL on a list that you save somewhere, so it'll persist.
You can save the list in a file, in user defaults, or in the persistent store's own metadata.
When it's time to sync, you'd do something like:
Get an NSURL from your list
Convert that into an NSManagedObjectID using managedObjectIDForURIRepresentation(url:NSURL) (which is on NSPersistentStoreCoordinator)
Get the managed object for that ID objectWithID: on NSManagedObjectContext.
Sync that object's data.
Then on a successful sync, remove entries from the list.

No array sent from parent app to Watch app

I am trying to receive an array of objects (that are retrieved from Parse within the app) from a parent application to be displayed in the watch application. I have been trying a few different things but with no success.
Here is my code in the extension:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
var parkPassed = context as! String
openParentAppWithPark(parkPassed)
}
private func openParentAppWithPark(park: String) {
WKInterfaceController.openParentApplication(["request": park], reply: { (reply, error) -> Void in
println(reply)
})
}
And the code in the parent app:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
println("Test")
if let userInfo = userInfo, request = userInfo["request"] as? NSArray {
if request == "Park 1" {
DataManager.sharedInstance.loadRides("Park 1")
} else if request == "Park 2" {
DataManager.sharedInstance.loadRides("Park 2")
} else if request == "Park 3" {
DataManager.sharedInstance.loadRides("Park 3")
} else {
DataManager.sharedInstance.loadRides("Park 4")
}
let rides = DataManager.sharedInstance.rideArray
println("Rides: \(rides)")
reply(["rideData": rides])
return
}
reply([:])
}
The println I have always returns nil the first time I try to load, and then [:] every other time. I assume this is because the request is timing out before the app has time to load the data from Parse? Also, the println that is supposed to print "Test" is never called.
In the extension, you're passing a String (park) to the parent application via the request key, but in the parent application, you're testing whether userInfo["request"] is an NSArray or not. You should be testing for a String, as in:
if let userInfo = userInfo, request = userInfo["request"] as? String {
First add a background task assertion to the openParentCall, you can find more context on that here: Background Task Watchkit
let backgroundTask = application.beginBackgroundTaskWithExpirationHandler { NSLog("TIME UP")}
///do code
reply(callback)
//
application.endBackgroundTask(backgroundId)
Now for the actual handleWatchKitExtensionRequest call I would change the first line to
if let request = userInfo["request"] as? String {
Now for the println("Test") not printing to console if you don't attach to process with the parentApplication then the println will not log out.
If the ride data is returning empty then I would inspect this function:
DataManager.sharedInstance.loadRides(ride: String)
make sure it is actually returning the correct data you need. Attach to process and place a breakpoint on each case and check that one of the cases is being called and also jump into the loadRides function to make sure it is coming back out from it. As a side note, the information you send back in the reply block has to be a property list or the reply block will always fail.

Resources