Crash during CoreData saving of multiple items - ios

I realize it's a long shot, but any suggestions would be helpful as I have struggled with this for quite some time. Once in a while, I get a crash while saving multiple items to core data. Attached is a stack. To me it seems that context is being destroyed while it is still being used or something like that. Perhaps I am doing something wrong. Code and crash image is below:
func insert(dtos: [DataType.DTO], finished: #escaping (_ error:Error?) -> Void) {
mutex.lock {
let convertProgress = Progress(totalUnitCount: Int64(dtos.count))
self.progress.addChild(convertProgress, withPendingUnitCount: 48)
//print("save dtos \(dtos)")
self.progress.completedUnitCount = 2
var retError: Error?
dtos.chunked(into: 10).forEach { chunk in
var objects = chunk.toUnsavedCoreData(in: context, progress: convertProgress)
context.performAndWait {
do {
try context.save()
} catch {
print("*************************\nERROR " + error.localizedDescription)
print(error._userInfo ?? "")
retError = error
}
}
objects.removeAll()
}
self.progress.completedUnitCount = 100
NotificationCenter.default.post(name: NSNotification.Name(rawValue: NSStringFromClass(DataType.self)), object: dtos)
finished(retError)
}
}
I am saving hundreds of objects so I am chunking them by 10 otherwise memory goes through the roof. It starts all here (assets are of asset types so types are saved first), once saved assets are stored:
let assettype = AssetTypeDataLayer()
let assetDl = AssetDataLayer(with:assettype.context)
let assets = assetDl.loadObjectsFromDB().toDTOs()
let types = assettype.loadObjectsFromDB().toDTOs()
assettype.save(dtos: types) { (error) in
if let _ = error {
} else {
assetDl.save(dtos: assets) { (error) in
}
}
override func save(dtos: [AssetType.DTO], finished: #escaping (_ error:Error?) -> Void) {
self.insert(dtos: $0, finished: finished)
}
Fatal Exception: NSInvalidArgumentException
*** -_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!

Related

How to wait for an API request to finish before storing data from function callback?

In a personal project of mine, I have created an API caller to retrieve a user's saved tracks from the Spotify API. The Spotify endpoint which I am using has a limit (maximum of 50 tracks per request) as well as an offset (starting index of first track in request), which is why I decided to use a FOR loop to get a series of track pages (each 50 tracks) and append them to a global array. The data is loaded from the main thread, and while the data is being requested, I display a child view controller with a spinner view. Once the data request has completed, I remove the spinner view, and transition to another view controller (passing the data as a property).
I have tried many things, but the array of tracks is always empty following the API request. I have a feeling it has to do with the synchronicity of my request, or maybe its possible that I'm not handling it correctly. Ideally, I would like to wait until the request from my API finishes, then append the result to the array. Do you have any suggestions on how I could solve this? Any help is much appreciated!
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
DispatchQueue.main.async { [weak self] in
if (self?.dropdownButton.dropdownLabel.text == "My saved music") {
self?.fetchSavedMusic() { tracksArray in
self?.tracksArray = tracksArray
}
}
...
self?.remove(asChildViewController: loadViewController)
self?.navigateToFilterScreen(tracksArray: self!.tracksArray)
}
}
private func fetchSavedMusic(completion: #escaping ([Tracks]) -> ()) {
let limit = 50
var offset = 0
var total = 200
for _ in stride(from: 0, to: total, by: limit) {
getSavedTracks(limit: limit, offset: offset) { tracks in
//total = tracks.total
self.tracksArray.append(tracks)
}
print(offset, limit)
offset = offset + 50
}
completion(tracksArray)
}
private func getSavedTracks(limit: Int, offset: Int, completion: #escaping (Tracks) -> ()) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { (result) in
switch result {
case .success(let model):
completion(model)
print("success")
case .failure(let error):
print("Error retrieving saved tracks: \(error.localizedDescription)")
print(error)
}
}
}
private func navigateToFilterScreen(tracksArray: [Tracks]) {
let vc = FilterViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
vc.paginatedTracks = tracksArray
show(vc, sender: self)
}
First you need to call completion when all of your data is loaded. In your case you call completion(tracksArray) before any of the getSavedTracks return.
For this part I suggest you to recursively accumulate tracks by going through all pages. There are multiple better tools to do so but I will give a raw example of it:
class TracksModel {
static func fetchAllPages(completion: #escaping ((_ tracks: [Track]?) -> Void)) {
var offset: Int = 0
let limit: Int = 50
var allTracks: [Track] = []
func appendPage() {
fetchSavedMusicPage(offset: offset, limit: limit) { tracks in
guard let tracks = tracks else {
completion(allTracks) // Most likely an error should be handled here
return
}
if tracks.count < limit {
// This was the last page because we got less than limit (50) tracks
completion(allTracks+tracks)
} else {
// Expecting another page to be loaded
offset += limit // Next page
allTracks += tracks
appendPage() // Recursively call for next page
}
}
}
appendPage() // Load first page
}
private static func fetchSavedMusicPage(offset: Int, limit: Int, completion: #escaping ((_ tracks: [Track]?) -> Void)) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { result in
switch result {
case .success(let model):
completion(model)
case .failure(let error):
print(error)
completion(nil) // Error also needs to call a completion
}
}
}
}
I hope comments will clear some things out. But the point being is that I nested an appendPage function which is called recursively until server stops sending data. In the end either an error occurs or the last page returns fewer tracks than provided limit.
Naturally it would be nicer to also forward an error but I did not include it for simplicity.
In any case you can now anywhere TracksModel.fetchAllPages { } and receive all tracks.
When you load and show your data (createSpinnerView) you also need to wait for data to be received before continuing. For instance:
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
TracksModel.fetchAllPages { tracks in
DispatchQueue.main.async {
self.tracksArray = tracks
self.remove(asChildViewController: loadViewController)
self.navigateToFilterScreen(tracksArray: tracks)
}
}
}
A few components may have been removed but I hope you see the point. The method should be called on main thread already. But you are unsure what thread the API call returned on. So you need to use DispatchQueue.main.async within the completion closure, not outside of it. And also call to navigate within this closure because this is when things are actually complete.
Adding situation for fixed number of requests
For fixed number of requests you can do all your requests in parallel. You already did that in your code.
The biggest problem is that you can not guarantee that responses will come back in same order than your requests started. For instance if you perform two request A and B it can easily happen due to networking or any other reason that B will return before A. So you need to be a bit more sneaky. Look at the following code:
private func loadPage(pageIndex: Int, perPage: Int, completion: #escaping ((_ items: [Any]?, _ error: Error?) -> Void)) {
// TODO: logic here to return a page from server
completion(nil, nil)
}
func load(maximumNumberOfItems: Int, perPage: Int, completion: #escaping ((_ items: [Any], _ error: Error?) -> Void)) {
let pageStartIndicesToRetrieve: [Int] = {
var startIndex = 0
var toReturn: [Int] = []
while startIndex < maximumNumberOfItems {
toReturn.append(startIndex)
startIndex += perPage
}
return toReturn
}()
guard pageStartIndicesToRetrieve.isEmpty == false else {
// This happens if maximumNumberOfItems == 0
completion([], nil)
return
}
enum Response {
case success(items: [Any])
case failure(error: Error)
}
// Doing requests in parallel
// Note that responses may return in any order time-wise (we can not say that first page will come first, maybe the order will be [2, 1, 5, 3...])
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count) { // Start with all nil
didSet {
// Yes, Swift can do this :D How amazing!
guard responses.contains(where: { $0 == nil }) == false else {
// Still waiting for others to complete
return
}
let aggregatedResponse: (items: [Any], errors: [Error]) = responses.reduce((items: [], errors: [])) { partialResult, response in
switch response {
case .failure(let error): return (partialResult.items, partialResult.errors + [error])
case .success(let items): return (partialResult.items + [items], partialResult.errors)
case .none: return (partialResult.items, partialResult.errors)
}
}
let error: Error? = {
let errors = aggregatedResponse.errors
if errors.isEmpty {
return nil // No error
} else {
// There was an error.
return NSError(domain: "Something more meaningful", code: 500, userInfo: ["all_errors": errors]) // Or whatever you wish. Perhaps just "errors.first!"
}
}()
completion(aggregatedResponse.items, error)
}
}
pageStartIndicesToRetrieve.enumerated().forEach { requestIndex, startIndex in
loadPage(pageIndex: requestIndex, perPage: perPage) { items, error in
responses[requestIndex] = {
if let error = error {
return .failure(error: error)
} else {
return .success(items: items ?? [])
}
}()
}
}
}
The first method is not interesting. It just loads a single page. The second method now collects all the data.
First thing that happens is we calculate all possible requests. We need a start index and per-page. So the pageStartIndicesToRetrieve for case of 145 items using 50 per page will return [0, 50, 100]. (I later found out we only need count 3 in this case but that depends on the API, so let's stick with it). We expect 3 requests starting with item indices [0, 50, 100].
Next we create placeholders for our responses using
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count)
for our example of 145 items and using 50 per page this means it creates an array as [nil, nil, nil]. And when all of the values in this array turn to not-nil then all requests have returned and we can process all of the data. This is done by overriding the setter didSet for a local variable. I hope the content of it speaks for itself.
Now all that is left is to execute all requests at once and fill the array. Everything else should just resolve by itself.
The code is not the easiest and again; there are tools that can make things much easier. But for academical purposes I hope this approach explains what needs to be done to accomplish your task correctly.

In memory realm loses data even though I am calling from same thread

NOTE: I have seen other posts but my problem is a little different
I have a helper class to access Realm. Every function in this class creates it's own instance of the Realm object to avoid thread issues, to be specific Realm accessed from incorrect thread.; This works perfectly fine for disk Realm; however, for my in memory realm the data is inserted successfully but when I try to retrieve it I get nothing. I thought maybe Realm is being accessed from different threads so what I did is I created a DispatchQueue and I always access realm from that queue.
Here is my code
protocol Cachable {}
protocol InMemoryCache {
func create<T: Cachable>(model: T.Type,
_ completion: #escaping (Result<T, Error>) -> ())
func save(object: Cachable,
_ completion: #escaping (Result<Void, Error>) -> ())
func fetch<T: Cachable>(model: T.Type,
predicate: NSPredicate?,
sorted: Sorted?,
_ completion: #escaping (Result<[T], Error>) -> ())
}
enum RealmInMemoryCacheError: Error {
case notRealmSpecificModel
case realmIsNil
case realmError
}
final class RealmInMemoryCache {
private let configuration: Realm.Configuration
private let queue: DispatchQueue
init(_ configuration: Realm.Configuration) {
self.queue = DispatchQueue(label: "inMemoryRealm", qos: .utility)
self.configuration = configuration
}
}
extension RealmInMemoryCache : InMemoryCache{
func create<T>(model: T.Type,
_ completion: #escaping (Result<T, Error>) -> ()) where T : Cachable {
self.queue.async {
guard let realm = try? Realm(configuration: self.configuration) else {
return completion(.failure(RealmInMemoryCacheError.realmIsNil))
}
guard let model = model as? RealmSwift.Object.Type else {
return completion(.failure(RealmInMemoryCacheError.notRealmSpecificModel))
}
do {
try realm.write { () -> () in
let newObject = realm.create(model, value: [], update: .all) as! T
return completion(.success(newObject))
}
} catch {
return completion(.failure(RealmInMemoryCacheError.realmError))
}
}
}
func save(object: Cachable,
_ completion: #escaping (Result<Void, Error>) -> ()) {
self.queue.async {
guard let realm = try? Realm(configuration: self.configuration) else {
return completion(.failure(RealmInMemoryCacheError.realmIsNil))
}
guard let object = object as? RealmSwift.Object else {
return completion(.failure(RealmInMemoryCacheError.notRealmSpecificModel))
}
do {
try realm.write { () -> () in
realm.add(object, update: .all)
return completion(.success(()))
}
} catch {
return completion(.failure(RealmInMemoryCacheError.realmError))
}
}
}
func fetch<T>(model: T.Type,
predicate: NSPredicate?,
sorted: Sorted?,
_ completion: #escaping (Result<[T], Error>) -> ()) where T : Cachable {
self.queue.async {
guard let realm = try? Realm(configuration: self.configuration) else {
return completion(.failure(RealmInMemoryCacheError.realmIsNil))
}
guard
let model = model as? RealmSwift.Object.Type else {
return completion(.failure(RealmInMemoryCacheError.notRealmSpecificModel))
}
var objects = realm.objects(model)
if let predicate = predicate {
objects = objects.filter(predicate)
}
if let sorted = sorted {
objects = objects.sorted(byKeyPath: sorted.key, ascending: sorted.ascending)
}
return completion(.success(objects.compactMap { $0 as? T}))
}
}
}
extension Object: Cachable {}
struct Sorted {
var key: String
var ascending: Bool = true
}
I eliminated code that doesn't add any benefit to the question hence you see empty/missing things in the above code. However, the code above works 100% copied and pasted.
I tried creating realm in the initialized instead so I have a strong reference to it; however, that causes issues with thread safety, it may work for few times but it would at some point crash the app due to the error Realm accessed from incorrect thread.
As you may tell, my goal is to make the above code generic and 100% thread safe even if called from a background thread say in a different function. Reason behind it is imagine the above class is an API and different programmers will use it, and sometimes they will call a function on a background thread for example without actually knowing what is going under the hood. I do not want the application to crash if they did such a thing.
EDIT: This is how the helper class is initialized
let realmInMemory = RealmInMemoryCache(Realm.Configuration(inMemoryIdentifier: "globalInMemoryRealm")
// Then I can use it like so (replace model with your realm model)
realmInMemory.create(model) { result in {
switch result {
...
}
}
EDIT 2: Here is a full example of how the above class works
import RealmSwift
final class MessageRealmEntity: Object {
#objc dynamic var id: String = ""
#objc dynamic var message: String = ""
convenience init(id: String, message: String) {
self.init()
self.id = id
self.message = message
}
override static func primaryKey() -> String? {
"id"
}
}
// THIS IS NOT PART OF THE PROBLEM, THIS `Main` CLASS IS JUST A DRIVER. THE CODE INSIDE IT COULD RUN ANYWHERE.
final class Main {
let realmInMemory = RealmInMemoryCache(Realm.Configuration(inMemoryIdentifier: "globalInMemoryRealm"))
func run() {
DispatchQueue.global(qos: .background).async {
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
var entity = MessageRealmEntity(id: "1", message: "Hello, World!")
self.realmInMemory.save(object: entity) { result in
switch result {
case .success(_):
print("Saved successfully")
case .failure(let error):
print("Got error")
}
semaphore.signal()
}
_ = semaphore.wait(wallTimeout: .distantFuture)
self.realmInMemory.fetch(model: MessageRealmEntity.self, predicate: nil, sorted: nil) { result in
switch result {
case .success(let messages):
print(messages.count) // This will return 0 when it should be 1 since we inserted already
case .failure(let error):
print("Got error")
}
semaphore.signal()
}
_ = semaphore.wait(wallTimeout: .distantFuture)
}
}
}
let main: Main = Main()
main.run()
All other methods are called the same way.
EDIT3:
I opened github issue if anyone is interested to follow it, here is the link: https://github.com/realm/realm-cocoa/issues/7017 there is a video and more explanation there
This is a github link to download a project to reproduce the bug https://github.com/Muhand/InMemoryRealm-Bug
After reading the documentation of Realm again and again and keep thinking of what #Jay have said in the comments, I paid more attention to this quote from the documentation
Notice: When all in-memory Realm instances with a particular identifier go out of scope with no references, all data in that Realm is deleted. We recommend holding onto a strong reference to any in-memory Realms during your app’s lifetime. (This is not necessary for on-disk Realms.)
Key sentence in the above quote is
When all in-memory Realm instances with a particular identifier go out of scope with no references, all data in that Realm is deleted.
In other words my Realm object is going out of scope everytime I try to save or fetch or do any other function
for example:
self.queue.async {
guard let realm = try? Realm(configuration: self.configuration) else {
completion(.failure(RealmInMemoryCacheError.realmIsNil))
return
}
guard let object = object as? RealmSwift.Object else {
completion(.failure(RealmInMemoryCacheError.notRealmSpecificModel))
return
}
do {
try realm.write { () -> () in
realm.add(object, update: .all)
completion(.success(()))
return
}
} catch {
completion(.failure(RealmInMemoryCacheError.realmError))
return
}
}
In the above code Realm goes out of scope as soon as that queue is done with it. Then Realm will look if there is any other variable in memory with the same identifier if so then it does nothing otherwise it will delete the current realm to optimize.
So the solution to the problem is basically to create a strong reference to realm with this identifier and then in each function re-create realm with the same identifier as well to avoid Realm accessed from incorrect thread This will however render in an extra variable that is not used but I think that is okay for now, at least it is my solution until something official comes from Realm. Keep in mind the process of re-initializing should not be an overhead since Realm takes care of that optimization.
Here is what I have done
final class RealmInMemoryCache {
private let configuration: Realm.Configuration
private let queue: DispatchQueue
private let strongRealm: Realm <-- Added variable
init(_ configuration: Realm.Configuration) {
self.queue = DispatchQueue(label: "inMemoryRealm", qos: .utility)
self.configuration = configuration
self.strongRealm = try! Realm(configuration: self.configuration) <-- Initialized here
}
}
and in other functions I do something like the following
...
self.queue.async {
guard let realm = try? Realm(configuration: self.configuration) else {
completion(.failure(RealmInMemoryCacheError.realmIsNil))
return
}
...
What threw me off thinking my Realm was going out of scope is when I setup breakpoints Realm started behaving perfectly fine. Although I still don't know 100% for sure why but my thinking is that xcode debugger might have created a strong reference to Realm when I write the command po realm.objects(...) in lldb.
I will accept this answer for now, unless someone has a better solution.

Saving multiple images to Parse

So I have an array of images I've accessed from my xcassets for demonstration purposes. There are 150 images I'm trying to save to my parse server at one time using parse frameworks. Here is the code I have so far. The problem I have is my app cpu goes to 100% in the tests and drops to 0. Also the images aren't saving to parse. I was hoping someone could help me find an efficient way to save 150 images to parse.
var imageNameList: [String] {
var imageNameList2:[String] = [] //[NSMutableArray]()
for i in 0...149 {
let imageName = String(format: "pic_%03d", Int(i))
imageNameList2.append(imageName)
}
return imageNameList2
}
#IBAction func Continue(_ sender: Any) {
for imageName in imageNameList {
var objectForSave:PFObject = PFObject(className: "Clo")
let object:UIImage = UIImage(named: imageName)!
let tilesPF = imageNameList.map({ name in
let data = UIImagePNGRepresentation(object as! UIImage)!
let file = PFFile(data: data)
let tile = PFObject(className: "Tile")
tile["tile"] = file
})
objectForSave["tiles"] = tilesPF
objectForSave.saveInBackground(block: { responseObject, error in
//you'll want to save the object ID of the PFObject if you want to retrieve a specific image later
})
}
}
The trouble is that the tight for-loop launches all of those requests concurrently causing some part of the http stack to bottleneck.
Instead, run the requests sequentially as follows (in my best approximation of Swift)...
func doOne(imageName: String, completion: (success: Bool)->()) {
var objectForSave:PFObject = PFObject(className: "Clo")
let object:UIImage = UIImage(named: imageName)!
// ... OP code that forms the request
objectForSave.saveInBackground(block: { responseObject, error in
success(error == nil)
})
}
func doMany(imageNames: Array<String>, completion: (success: Bool)->()) {
if (imageNames.count == 0) return completion(YES)
let nextName = imageNames[0];
self.doOne(imageName:imageNames[0] completion: {(success: Bool) -> Void in
if (success) {
let remainingNames = imageNames[1..imageNames.count-1]
self.doMany(imageNames: remainingNames completion:completion)
} else {
completion(NO)
})
}
In English, just in case I goofed the Swift, the idea is to factor out a single request into it's own function with a completion handler. Build a second function that takes an array of arguments to the network request, and use that array like a to-do list: do the first item on the list, when it completes, call itself recursively to do the remaining items.

Running one function after another completes

I am trying to run loadViews() after the pullData() completes and I am wondering what the best way of doing this is? I would like to set a 10 sec timeout on it as well so I can display a network error if possible. From what I have read, GCD looks like it is the way to accomplish this but I am confused on the implementation of it. Thanks for any help you can give!
//1
pullData()
//2
loadViews()
What you need is a completion handler with a completion block.
Its really simple to create one:
func firstTask(completion: (success: Bool) -> Void) {
// Do something
// Call completion, when finished, success or faliure
completion(success: true)
}
And use your completion block like this:
firstTask { (success) -> Void in
if success {
// do second task if success
secondTask()
}
}
You can achieve like this :-
func demo(completion: (success: Bool) -> Void) {
// code goes here
completion(success: true)
}
I had a similar situation where I had to init a view once the data is pulled from Parse server. I used the following:
func fetchQuestionBank(complete:()->()){
let userDefault = NSUserDefaults.standardUserDefaults()
let username = userDefault.valueForKey("user_email") as? String
var query = PFQuery(className:"QuestionBank")
query.whereKey("teacher", equalTo: username!)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil {
if let objects = objects as? [PFObject] {
var questionTitle:String?
var options:NSArray?
for (index, object) in enumerate(objects) {
questionTitle = object["question_title"] as? String
options = object["options"] as? NSArray
var aQuestion = MultipleChoiceQuestion(questionTitle: questionTitle!, options: options!)
aQuestion.questionId = object.objectId!
InstantlyModel.sharedInstance.questionBank.append(aQuestion)
}
complete()
}
}else{
println(" Question Bank Error \(error) ")
}
}
}
And this is you call the method:
self.fetchQuestionBank({ () -> () in
//Once all the data pulled from server. Show Teacher View.
self.teacherViewController = TeacherViewController(nibName: "TeacherViewController", bundle: nil)
self.view.addSubview(self.teacherViewController!.view)
})
function1();
function2();
Use functions!! Once function1() function completed, function2() will execute.

Why doesn't HKAnchoredObjectQuery with enableBackgroundDeliveryForType always fire when app is in the background?

I'm experimenting a bit to familiarize myself with the HKAnchoredObjectQuery and getting results when my app is inactive.
I start the app, switch away to Apple Health, enter a blood glucose result; sometimes the results handler is called right away (as evidenced by the print to the console) but other times the handler isn't called until I switch back to my app. Same is true for deleted results as well as added results. Anybody have any guidance?
Most of this code is from a question from thedigitalsean adapted here to get updates while app is in the background and logging to the console. See: Healthkit HKAnchoredObjectQuery in iOS 9 not returning HKDeletedObject
class HKClient : NSObject {
var isSharingEnabled: Bool = false
let healthKitStore:HKHealthStore? = HKHealthStore()
let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
override init(){
super.init()
}
func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) {
let dataTypesToRead : Set<HKObjectType> = [ glucoseType ]
if(!HKHealthStore.isHealthDataAvailable())
{
// let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"])
self.isSharingEnabled = false
return
}
self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in
self.isSharingEnabled = true
authorizationCompleted(success: success, error: error)
}
}
func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:uint, callback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!) {
let queryEndDate = NSDate(timeIntervalSinceNow: NSTimeInterval(60.0 * 60.0 * 24))
let queryStartDate = NSDate.distantPast()
let sampleType: HKSampleType = glucoseType as! HKSampleType
let predicate: NSPredicate = HKAnchoredObjectQuery.predicateForSamplesWithStartDate(queryStartDate, endDate: queryEndDate, options: HKQueryOptions.None)
var hkAnchor: HKQueryAnchor
if(anchor != nil){
hkAnchor = anchor!
} else {
hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
}
let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = {
(query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in
var added = [String]()
var deleted = [String]()
if (addedObjects?.count > 0){
for obj in addedObjects! {
let quant = obj as? HKQuantitySample
if(quant?.UUID.UUIDString != nil){
let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! )
let msg : String = (quant?.UUID.UUIDString)! + " " + String(val)
added.append(msg)
}
}
}
if (deletedObjects?.count > 0){
for del in deletedObjects! {
let value : String = del.UUID.UUIDString
deleted.append(value)
}
}
if(callback != nil){
callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError)
}
}
// remove predicate to see deleted objects
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults)
// added - query should be always running
anchoredQuery.updateHandler = onAnchorQueryResults
// added - allow query to pickup updates when app is in backgroun
healthKitStore?.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate) {
(success, error) in
if (!success) {print("enable background error")}
}
healthKitStore?.executeQuery(anchoredQuery)
}
let AnchorKey = "HKClientAnchorKey"
func getAnchor() -> HKQueryAnchor? {
let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey)
if(encoded == nil){
return nil
}
let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor
return anchor
}
func saveAnchor(anchor : HKQueryAnchor) {
let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor)
NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
class ViewController: UIViewController {
let debugLabel = UILabel(frame: CGRect(x: 10,y: 20,width: 350,height: 600))
override func viewDidLoad() {
super.viewDidLoad()
self.view = UIView();
self.view.backgroundColor = UIColor.whiteColor()
debugLabel.textAlignment = NSTextAlignment.Center
debugLabel.textColor = UIColor.blackColor()
debugLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
debugLabel.numberOfLines = 0
self.view.addSubview(debugLabel)
let hk = HKClient()
hk.requestGlucosePermissions(){
(success, error) -> Void in
if(success){
let anchor = hk.getAnchor()
hk.getGlucoseSinceAnchor(anchor, maxResults: 0)
{ (source, added, deleted, newAnchor, error) -> Void in
var msg : String = String()
if(deleted?.count > 0){
msg += "Deleted: \n" + (deleted?[0])!
for s in deleted!{
msg += s + "\n"
}
}
if (added?.count > 0) {
msg += "Added: "
for s in added!{
msg += s + "\n"
}
}
if(error != nil) {
msg = "Error = " + (error?.description)!
}
if(msg.isEmpty)
{
msg = "No changes"
}
debugPrint(msg)
if(newAnchor != nil && newAnchor != anchor){
hk.saveAnchor(newAnchor!)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.debugLabel.text = msg
})
}
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I also added print()'s at the various application state changes. A sample of the console log (this is running on iPhone 6s device from XCode) shows the handler being called sometimes after I entered background but before reentering foreground and other times only after reentering foreground.
app did become active
"No changes"
app will resign active
app did enter background
app will enter foreground
"Added: E0340084-6D9A-41E4-A9E4-F5780CD2EADA 99.0\n"
app did become active
app will resign active
app did enter background
"Added: CEBFB656-0652-4109-B994-92FAA45E6E55 98.0\n"
app will enter foreground
"Added: E2FA000A-D6D5-45FE-9015-9A3B9EB1672C 97.0\n"
app did become active
app will resign active
app did enter background
"Deleted: \nD3124A07-23A7-4571-93AB-5201F73A4111D3124A07-23A7-4571-93AB-5201F73A4111\n92244E18-941E-4514-853F-D890F4551D76\n"
app will enter foreground
app did become active
app will resign active
app did enter background
app will enter foreground
"Added: 083A9DE4-5EF6-4992-AB82-7CDDD1354C82 96.0\n"
app did become active
app will resign active
app did enter background
app will enter foreground
"Added: C7608F9E-BDCD-4CBC-8F32-94DF81306875 95.0\n"
app did become active
app will resign active
app did enter background
"Deleted: \n15D5DC92-B365-4BB1-A40C-B870A48A70A415D5DC92-B365-4BB1-A40C-B870A48A70A4\n"
"Deleted: \n17FB2A43-0828-4830-A229-7D7DDC6112DB17FB2A43-0828-4830-A229-7D7DDC6112DB\n"
"Deleted: \nCEBFB656-0652-4109-B994-92FAA45E6E55CEBFB656-0652-4109-B994-92FAA45E6E55\n"
app will enter foreground
"Deleted: \nE0340084-6D9A-41E4-A9E4-F5780CD2EADAE0340084-6D9A-41E4-A9E4-F5780CD2EADA\n"
app did become active
I suggest using an HKObserverQuery and setting it up carefully.
There is an algorithm that watches how and when you call the "completion" handler of the HKObserverQuery when you have background delivery enabled. The details of this are vague unfortunately. Someone on the Apple Dev forums called it the "3 strikes" rule but Apple hasn't published any docs that I can find on it's behavior.
https://forums.developer.apple.com/thread/13077
One thing I have noticed is that, if your app is responding to a background delivery with an HKObserverQuery, creating an HKAnchoredObjectQuery, and setting the UpdateHandler in that HKAnchoredObjectQuery, this UpdateHandler will often cause multiple firings of the callback. I suspected that perhaps since these additional callbacks are being executed AFTER you have already told Apple that you have completed you work in response to the background delivery, you are calling the completion handler multiple times and maybe they ding you some "points" and call you less often for bad behavior.
I had the most success with getting consistent callbacks by doing the following:
Using an ObserverQuery and making the sure the call of the "completion" handler gets called once and at the very end of your work.
Not setting an update handler in my HKAnchoredObjectQuery when running in the background (helps achieve 1).
Focusing on making my query handlers, AppDelegate, and ViewController are as fast as possible. I noticed that when I reduced all my callbacks down to just a print statement, the callbacks from HealthKit came immediately and more consistently. So that says Apple is definitely paying attention to execution time. So try to statically declare things where possible and focus on speed.
I have since moved on to my original project which uses Xamarin.iOS, not swift, so I haven't kept up with the code I originally posted. But here is an updated (and untested) version of that code that should take these changes into account (except for the speed improvements):
//
// HKClient.swift
// HKTest
import UIKit
import HealthKit
class HKClient : NSObject {
var isSharingEnabled: Bool = false
let healthKitStore:HKHealthStore? = HKHealthStore()
let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
override init(){
super.init()
}
func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) {
let dataTypesToRead : Set<HKObjectType> = [ glucoseType ]
if(!HKHealthStore.isHealthDataAvailable())
{
// let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"])
self.isSharingEnabled = false
return
}
self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in
self.isSharingEnabled = true
authorizationCompleted(success: success, error: error)
}
}
func startBackgroundGlucoseObserver( maxResultsPerQuery: Int, anchorQueryCallback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!)->Void {
let onBackgroundStarted = {(success: Bool, nsError : NSError?)->Void in
if(success){
//Background delivery was successfully created. We could use this time to create our Observer query for the system to call when changes occur. But we do it outside this block so that even when background deliveries don't work,
//we will have the observer query working when are in the foreground at least.
} else {
debugPrint(nsError)
}
let obsQuery = HKObserverQuery(sampleType: self.glucoseType as! HKSampleType, predicate: nil) {
query, completion, obsError in
if(obsError != nil){
//Handle error
debugPrint(obsError)
abort()
}
var hkAnchor = self.getAnchor()
if(hkAnchor == nil) {
hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
}
self.getGlucoseSinceAnchor(hkAnchor, maxResults: maxResultsPerQuery, callContinuosly:false, callback: { (source, added, deleted, newAnchor, error) -> Void in
anchorQueryCallback(source: self, added: added, deleted: deleted, newAnchor: newAnchor, error: error)
//Tell Apple we are done handling this event. This needs to be done inside this handler
completion()
})
}
self.healthKitStore?.executeQuery(obsQuery)
}
healthKitStore?.enableBackgroundDeliveryForType(glucoseType, frequency: HKUpdateFrequency.Immediate, withCompletion: onBackgroundStarted )
}
func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:Int, callContinuosly:Bool, callback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!){
let sampleType: HKSampleType = glucoseType as! HKSampleType
var hkAnchor: HKQueryAnchor;
if(anchor != nil){
hkAnchor = anchor!
} else {
hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
}
let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = {
(query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in
var added = [String]()
var deleted = [String]()
if (addedObjects?.count > 0){
for obj in addedObjects! {
let quant = obj as? HKQuantitySample
if(quant?.UUID.UUIDString != nil){
let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! )
let msg : String = (quant?.UUID.UUIDString)! + " " + String(val)
added.append(msg)
}
}
}
if (deletedObjects?.count > 0){
for del in deletedObjects! {
let value : String = del.UUID.UUIDString
deleted.append(value)
}
}
if(callback != nil){
callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError)
}
}
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults)
if(callContinuosly){
//The updatehandler should not be set when responding to background observerqueries since this will cause multiple callbacks
anchoredQuery.updateHandler = onAnchorQueryResults
}
healthKitStore?.executeQuery(anchoredQuery)
}
let AnchorKey = "HKClientAnchorKey"
func getAnchor() -> HKQueryAnchor? {
let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey)
if(encoded == nil){
return nil
}
let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor
return anchor
}
func saveAnchor(anchor : HKQueryAnchor) {
let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor)
NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey)
NSUserDefaults.standardUserDefaults().synchronize()
}
}

Resources