Error Handling When Saving a CKRecord - ios

I am looking for an example of proper error handling when saving a CKRecord. According to the Apple docs I should "Use the information in the error object to determine whether a problem has a workaround."
I understand that the error object has a userInfo dictionary, but how do I figure out what the keys are for the dictionary and how to handle the errors?
The following example illustrates how I'm currently saving a CKRecord:
CKRecord *record = [[CKRecord alloc] initWithRecordType:#"MyRecordType"];
[record setValue:[NSNumber numberWithInt:99] forKey:#"myInt"];
[db saveRecord:record completionHandler:^(CKRecord *savedPlace, NSError *error) {
// handle errors here
if (savedPlace) {
NSLog(#"save successful");
}else{
NSLog(#"save unsuccessful");
}
if (error) {
NSLog(#"Error saving %#", error.localizedDescription);
}
}];
How can I improve this code to provide work arounds for potential saving issues?

In my library EVCloudKitDao I have a separate method that will return a error type based on the error code. Depending on that type you can decide what to do. Here is that method:
public enum HandleCloudKitErrorAs {
case Success,
Retry(afterSeconds:Double),
RecoverableError,
Fail
}
public static func handleCloudKitErrorAs(error:NSError?, retryAttempt:Double = 1) -> HandleCloudKitErrorAs {
if error == nil {
return .Success
}
let errorCode:CKErrorCode = CKErrorCode(rawValue: error!.code)!
switch errorCode {
case .NetworkUnavailable, .NetworkFailure, .ServiceUnavailable, .RequestRateLimited, .ZoneBusy, .ResultsTruncated:
// Use an exponential retry delay which maxes out at half an hour.
var seconds = Double(pow(2, Double(retryAttempt)))
if seconds > 1800 {
seconds = 1800
}
// Or if there is a retry delay specified in the error, then use that.
if let userInfo = error?.userInfo {
if let retry = userInfo[CKErrorRetryAfterKey] as? NSNumber {
seconds = Double(retry)
}
}
NSLog("Debug: Should retry in \(seconds) seconds. \(error)")
return .Retry(afterSeconds: seconds)
case .UnknownItem, .InvalidArguments, .IncompatibleVersion, .BadContainer, .MissingEntitlement, .PermissionFailure, .BadDatabase, .AssetFileNotFound, .OperationCancelled, .NotAuthenticated, .AssetFileModified, .BatchRequestFailed, .ZoneNotFound, .UserDeletedZone, .InternalError, .ServerRejectedRequest, .ConstraintViolation:
NSLog("Error: \(error)")
return .Fail;
case .QuotaExceeded, .LimitExceeded:
NSLog("Warning: \(error)")
return .Fail;
case .ChangeTokenExpired, .ServerRecordChanged:
NSLog("Info: \(error)")
return .RecoverableError
default:
NSLog("Error: \(error)") //New error introduced in iOS...?
return .Fail;
}
}
Inside the callback of a CloudKit method you can then use this function like this:
func loadContacts(retryCount:Double = 1) {
// Look who of our contact is also using this app.
EVCloudKitDao.publicDB.allContactsUserInfo({ users in
EVLog("AllContactUserInfo count = \(users.count)");
Async.main{
self.contacts = users
self.tableView.reloadData()
}
}, errorHandler: { error in
switch EVCloudKitDao.handleCloudKitErrorAs(error, retryAttempt: retryCount) {
case .Retry(let timeToWait):
Async.background(after: timeToWait) {
self.loadContacts(retryCount + 1)
}
case .Fail:
Helper.showError("Something went wrong: \(error.localizedDescription)")
default: // For here there is no need to handle the .Success, .Fail and .RecoverableError
break
}
})
}
In my the case above I use a separate error callback handler. You can also call it directly form within a CloudKit method callback. Just first check if there is an error.

Here is an implementation in which I handle the common error of CKErrorNetworkFailure by retrying to save after the recommended retry after time interval which is stored in the userInfo dictionary.
-(void)saveRecord:(CKRecord*)record toDatabase:(CKDatabase*)database{
[database saveRecord:record completionHandler:^(CKRecord *record, NSError *error) {
if (error==nil) {
NSLog(#"The save was successful");
//Do something
}else{
NSLog(#"Error saving with localizedDescription: %#", error.localizedDescription);
NSLog(#"CKErrorCode = %lu", [error code]);
if ([error code]==CKErrorNetworkFailure) {
double retryAfterValue = [[error.userInfo valueForKey:CKErrorRetryAfterKey] doubleValue];
NSLog(#"Error code network unavailable retrying after %f", retryAfterValue);
NSTimer *timer = [NSTimer timerWithTimeInterval:retryAfterValue target:self selector:#selector(testOutCloudKit) userInfo:nil repeats:NO];
[timer fire];
}
}
}];
}

In Swift 3, I handle my CloudKit errors this way:
privateDB.perform(myQuery, inZoneWith: nil) {records, error in
if error != nil {
print (error?.localizedDescription)
if error?._code == CKError.notAuthenticated.rawValue {
// Solve problems here...
}
Note that the CKError.notAuthenticated is selected from the list presented by code completion after writing CKError.
Hope this helps!

Related

Apple Health initHeartBeatSeries , how to get HKHeartbeatSeriesSample?

Trying to get HeartbeatSeries working but not sure how to get HkHeartbeatSeriesSample. Here's my code
I have this query which is gonna return the data from HeartbeatSeries but I'm not sure how to get the HKHeartbeatSeriesSample
built the query from here
https://developer.apple.com/documentation/healthkit/hkheartbeatseriesquery/3113764-initwithheartbeatseries?language=objc
-(void)fetchHeartSeries:(HKHeartbeatSeriesSample *)sample
timeSinceStart: (NSTimeInterval *)timeSinceStart
completion:(void (^)(NSArray *, NSError *))completionHandler API_AVAILABLE(ios(13.0)){
HKHeartbeatSeriesSample *sampleSeries = sample;
NSTimeInterval *timeSince = timeSinceStart;
if (#available(iOS 13.0, *)) {
HKHeartbeatSeriesQuery *query = [
[HKHeartbeatSeriesQuery alloc]
initWithHeartbeatSeries:(HKHeartbeatSeriesSample *)sampleSeries
dataHandler:^(HKHeartbeatSeriesQuery *query,
NSTimeInterval timeSince,
BOOL precededByGap,
BOOL done,
NSError * error){
if (error) {
// Perform proper error handling here
NSLog(#"*** An error occurred while getting the heart beat series: %# ***", error.localizedDescription);
completionHandler(nil, error);
}
if(done){
NSArray *data = query.accessibilityElements;
NSLog(#"Successfully retrieved heart beat data");
completionHandler(data, nil);
}
}];
[self.healthStore executeQuery:query];
} else {
// Fallback on earlier versions
}
}
This is how I did it. Note, you need to add HKSeriesType.heartbeat() as one of the read types in the requestAuthorization function to have permissions to get the Beat-to-Beat Measurements.
A simple, proof-of-concept to grab the HKHeartbeatSeriesSamples from the last 2 hours and use the first one to get the beat-to-beat measurements and print out the timestamp differences from the start.
I apologize for using Swift here. Let me know and I can provide an Objective-C version.
let last2hours = HKQuery.predicateForSamples(withStart: Date().addingTimeInterval(-60 * 60 * 24), end: Date(), options: [])
let hbSeriesSampleType = HKSeriesType.heartbeat()
let heartbeatSeriesSampleQuery = HKSampleQuery(sampleType: hbSeriesSampleType, predicate: last2hours, limit: 20, sortDescriptors: nil) { (sampleQuery, samples, error) in
if let heartbeatSeriesSample = samples?.first as? HKHeartbeatSeriesSample {
let query = HKHeartbeatSeriesQuery(heartbeatSeries: heartbeatSeriesSample) { (query, timeSinceSeriesStart, precededByGap, done, error) in
print(timeSinceSeriesStart)
}
self.healthStore.execute(query)
}
}
healthStore.execute(heartbeatSeriesSampleQuery)
For this HKHeartbeatSeriesSample:
count=23 F7D641F8-07AD-4543-84C8-126EA7B98B0F "Eric’s Apple Watch" (7.3.3), "Watch6,2" (7.3.3) "Apple Watch" (2021-04-13 17:18:59 -0500 - 2021-04-13 17:19:59 -0500)
Code above prints out to console:
0.78125
1.5390625
2.296875
3.08203125
3.87109375
4.61328125
5.37109375
6.10546875
6.86328125
7.7109375
9.3359375
10.94921875
11.76953125
12.5625
20.05078125
20.84765625
21.625
22.45703125
32.62109375
33.36328125
34.08203125
34.8046875
35.53515625

Not getting data from server with [FIRRemoteConfig remoteConfig]

In the iOS code below, the status of the fetch is FIRRemoteConfigFetchStatusSuccess. When activateFetched is applied in the handler, the result is true. It looks to me therefore as if it should be the case that you can access the remote config values from the server. However, it is only the local value that is obtained when do [FIRRemoteConfig remoteConfig][#"greeting"].stringValue;
On Firebase console have set a parameter called "greeting". What possible reasons are there to explain why it is not retrieving the server value for this parameter?
- (void)fetchFirebaseRemoteConfig {
long expirationDuration = 43200;
if ([FIRRemoteConfig remoteConfig].configSettings.isDeveloperModeEnabled) {
expirationDuration = 0;
}
[[FIRRemoteConfig remoteConfig] fetchWithExpirationDuration:expirationDuration completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {
if (status == FIRRemoteConfigFetchStatusSuccess && error == nil) {
BOOL didApply = [[FIRRemoteConfig remoteConfig] activateFetched];
ALog("Did apply remote config OK: %d", didApply);
} else {
ALog(#"Error %#", error.localizedDescription);
}
NSString *greeting = [FIRRemoteConfig remoteConfig][#"greeting"].stringValue;
ALog(#"greeting: %#", greeting);
}];
}

Firestore iOS queryWhereField and queryOrderedByField is not returning specific data

In my iOS project, I'm using Firestore. I'm in need to use queryWhereField and queryOrderedByField, I've implemented as follows but it's not working:
defFirestore = [FIRFirestore firestore];
colRef = [defFirestore collectionWithPath:#"walks"];
[[[colRef queryWhereField:#"requestId" isEqualTo:#(self.requestId)] queryWhereField:#"taskId" isEqualTo:#(self.taskId)] queryOrderedByField:#"createdDate"];
[colRef getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error != nil) {
NSLog(#"Error getting documents: %#", error);
} else {
for (FIRDocumentSnapshot *document in snapshot.documents) {
NSLog(#"%# => %#", document.documentID, document.data);
}
}
}];
Above query is not returning where specific record rather returning all records also not ordered by createdDate field. Firestore database screenshot as follows:
Okay, I've already figured that out. First of all need to create an index in Firestore for those fields we want to query from. then need to define a FIRQuery to run the query with those fields. So the query will look like as follows:
FIRQuery *walks = [[[colRef queryWhereField:#"requestId" isEqualTo:#(self.requestId)] queryWhereField:#"taskId" isEqualTo:#(self.taskId)] queryOrderedByField:#"createdDate"];
[walks addSnapshotListener:^(FIRQuerySnapshot *snapshot, NSError *error) {
if (snapshot == nil) {
NSLog(#"Error fetching documents: %#", error);
return;
}
if (error != nil) {
NSLog(#"Error getting documents: %#", error);
} else {
for (FIRDocumentChange *diff in snapshot.documentChanges) {
if (diff.type == FIRDocumentChangeTypeAdded) {
NSLog(#"%# => %#", diff.document.documentID, diff.document.data);
}
}
}
}];
I used snapshot because I want to get realtime update.

Prevent a closure from running until another has completed

Here is code for two closures in two different IBAction button presses. The desired outcome is for the button press to turn on/off an LED, then to access a light sensor and read the light value after the change in LED status.
What happens is a race condition where the function getVariable runs and returns before the callFunction has implemented the change. The result is that the value displayed in getLightLabel.text is that of the prior condition, not the current condition.
My question is how to rewrite the code below so that myPhoton!.getVariable does not execute until after the myPhoton!.callFunction has returned (completed its task).
I have tried placing getVariable inside callFunction, both before and after the } closing if (error == nil), but the result was identical to the code shown here.
#IBAction func lightOn(sender: AnyObject) {
let funcArgs = [1]
myPhoton!.callFunction("lightLed0", withArguments: funcArgs) { (resultCode : NSNumber!, error : NSError!) -> Void in
if (error == nil) {
self.lightStateLabel.text = "LED is on"
}
}
myPhoton!.getVariable("Light", completion: { (result:AnyObject!, error:NSError!) -> Void in
if let e = error {
self.getLightLabel.text = "Failed reading light"
}
else {
if let res = result as? Float {
self.getLightLabel.text = "Light level is \(res) lumens"
}
}
})
}
#IBAction func lightOff(sender: AnyObject) {
let funcArgs = [0]
myPhoton!.callFunction("lightLed0", withArguments: funcArgs) { (resultCode : NSNumber!, error : NSError!) -> Void in
if (error == nil) {
self.lightStateLabel.text = "LED is off"
}
}
myPhoton!.getVariable("Light", completion: { (result:AnyObject!, error:NSError!) -> Void in
if let e = error {
self.getLightLabel.text = "Failed reading light"
}
else {
if let res = result as? Float {
self.getLightLabel.text = "Light level is \(res) lumens"
}
}
})
}
Here is the callFunction comments and code from the .h file. This SDK is written in Objective C. I am using it in Swift with a bridging header file.
/**
* Call a function on the device
*
* #param functionName Function name
* #param args Array of arguments to pass to the function on the device. Arguments will be converted to string maximum length 63 chars.
* #param completion Completion block will be called when function was invoked on device. First argument of block is the integer return value of the function, second is NSError object in case of an error invoking the function
*/
-(void)callFunction:(NSString *)functionName withArguments:(NSArray *)args completion:(void (^)(NSNumber *, NSError *))completion;
/*
-(void)addEventHandler:(NSString *)eventName handler:(void(^)(void))handler;
-(void)removeEventHandler:(NSString *)eventName;
*/
Here is the .m file code
-(void)callFunction:(NSString *)functionName withArguments:(NSArray *)args completion:(void (^)(NSNumber *, NSError *))completion
{
// TODO: check function name exists in list
NSURL *url = [self.baseURL URLByAppendingPathComponent:[NSString stringWithFormat:#"v1/devices/%#/%#", self.id, functionName]];
NSMutableDictionary *params = [NSMutableDictionary new]; //[self defaultParams];
// TODO: check response of calling a non existant function
if (args) {
NSMutableArray *argsStr = [[NSMutableArray alloc] initWithCapacity:args.count];
for (id arg in args)
{
[argsStr addObject:[arg description]];
}
NSString *argsValue = [argsStr componentsJoinedByString:#","];
if (argsValue.length > MAX_SPARK_FUNCTION_ARG_LENGTH)
{
// TODO: arrange user error/codes in a list
NSError *err = [self makeErrorWithDescription:[NSString stringWithFormat:#"Maximum argument length cannot exceed %d",MAX_SPARK_FUNCTION_ARG_LENGTH] code:1000];
if (completion)
completion(nil,err);
return;
}
params[#"args"] = argsValue;
}
[self setAuthHeaderWithAccessToken];
[self.manager POST:[url description] parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completion)
{
NSDictionary *responseDict = responseObject;
if ([responseDict[#"connected"] boolValue]==NO)
{
NSError *err = [self makeErrorWithDescription:#"Device is not connected" code:1001];
completion(nil,err);
}
else
{
// check
NSNumber *result = responseDict[#"return_value"];
completion(result,nil);
}
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
if (completion)
completion(nil,error);
}];
}
One solution is to put the second closure inside the first, where the first returns and provides and Error value. If no error,then execuet the second closure. That is one way to tightly couple the two closures without resorting to semaphores or other messaging schemes.
In this application, the problem I was encountering cannot be solved on the IOS/Swift side of the stack. The cloud API and embedded uP are not tightly coupled, so the cloud returns to the IOS with a completion before the full function code has run on the Particle uP.
The solution to this overall problem actually lies in either modifying the cloud API or adding some additional code to the uP firmware to tightly couple the process to the IOS app with additional communication.

StrongLoop Loopback example in Swift

I'm trying to implement the example LoopBack iOS app in Swift
Create a LoopBack iOS app: part one
and I'm having some trouble translating from the ObjectiveC
- (void) getBooks
{
//Error Block
void (^loadErrorBlock)(NSError *) = ^(NSError *error){
NSLog(#"Error on load %#", error.description);
};
void (^loadSuccessBlock)(NSArray *) = ^(NSArray *models){
NSLog(#"Success count %d", models.count);
self.tableData = models;
[self.myTable reloadData];
};
//This line gets the Loopback model "book" through the adapter defined in AppDelegate
LBModelRepository *allbooks = [[booksAppDelegate adapter] repositoryWithModelName:prototypeName];
//Logic - Get all books. If connection fails, load the error block, if it passes, call the success block and pass allbooks to it.
[allbooks allWithSuccess:loadSuccessBlock failure:loadErrorBlock];
};
Here's my version
func getBooks() {
var errorBlock = {
(error: NSError!) -> Void in
NSLog("Error on load %#", error.description)
}
var successBlock = {
(models: NSArray!) -> Void in
NSLog("Success count %d", models.count)
self.tableData = models
self.booksTable.reloadData()
}
// get the "book" model
var allBooks: LBModelRepository = adapter.repositoryWithModelName(prototypeName)
// get all books
allBooks.allWithSuccess(successBlock, errorBlock)
}
but I get a compiler error on the call to allWithSuccess:
Cannot convert the expressions type 'Void' to type 'LBModelAllSuccessBlock!'
What am I missing?
UPDATE:
If I declare the success block as follows, it works:
var successBlock = {
(models: AnyObject[]!) -> () in
self.tableData = models
self.booksTable.reloadData()
}
Thanks for the answer!!!!
If anyone is looking for the last version of Swift and LoopBack iOS SDK, it worked for me like this:
func getBooks() {
// Error Block
let errorBlock = {
(error: NSError!) -> Void in
NSLog("Error on load %#", error.description)
}
// Success Block
let successBlock = {
(models: [AnyObject]!) -> () in
self.tableData = models
self.myTable.reloadData()
}
// This line gets the Loopback model "book" through the adapter defined in AppDelegate
let allBooks:LBPersistedModelRepository = AppDelegate.adapter.repositoryWithModelName(prototypeName, persisted: true) as! LBPersistedModelRepository
// Logic - Get all books. If connection fails, load the error block, if it passes, call the success block and pass allbooks to it.
allBooks.allWithSuccess(successBlock, failure: errorBlock)
}

Resources