Realm-cocoa: How to add notification inside other notification block - ios

In my app, I have registered a notification block that itself has to register an other notifiaction block.
notificationToken = [self.appState.currentProject addNotificationBlock:^(BOOL deleted, NSArray<RLMPropertyChange *> *changes, NSError *error) {
if (deleted) {
NSLog(#"The object was deleted.");
} else if (error) {
NSLog(#"An error occurred: %#", error);
} else {
bool filterChanged = false;
for (RLMPropertyChange * change in changes){
if([change.name isEqualToString:#"filters"])
filterChanged = true;
}
if(filterChanged){
// compute predicate from filters
// predicate = [NSPredicate ...
styles = [Style objectsWithPredicate:predicate]
notificationTokenStyles = [styles addNotificationBlock:^(RLMResults<Style *> * _Nullable results, RLMCollectionChange * _Nullable change, NSError * _Nullable error) {
// react on changed styles.
}]; // <- RLMException here
}
}];
}
}];
I know that this is technically not possible, but for me it is necessary. Can I somehow dispatch or defer the nested addNotificationBlock to another thread or so? I know I could wait 500 ms, but that would result in a race condition.
Now, my problem is that as soon as I perform a wirte transaction that leads to the first notification block to execute, I encounter RLMException: Cannot register notification blocks from within write transactions. just at the nested addNotificationBlock.
Thanks in advance!

This is a known problem (see this Realm issue on GitHub).
You can either wrap the creation of the new NotificationToken like this (Swift code):
DispatchQueue.main.async {
// register observation blocks here
}
Or you can call realm.refresh() before starting the write transaction.

Related

Cannot register notification blocks from within write transactions

I am trying to add notification block for my object like this:
self.tokenChat = [self.chat addNotificationBlock:^(BOOL deleted, NSArray<RLMPropertyChange *> * _Nullable changes, NSError * _Nullable error) {
if (!deleted && ![weakSelf.chat isInvalidated]) {
[weakSelf.tableMes reloadData];
}
}];
But having error like: Cannot register notification blocks from within write transactions.
What's possible reasons for this behaviour?
Realm does not allow create observers within write transactions. More details in this thread.

Concurrency iterating over NSArray

I've an array of user objects. I need to iterate over this array, and then, for each user, I need to make a request to fetch data, based on its user id. This request is an async request (and for reasons I can't make it sync). Here is the edited code using dispatch_group calls:
EDITED
for (User *user in self.userList) {
dispatch_group_enter(self.usersGroup);
[self.wrapper getUserDataWithId:user.userid completion:^(Owner *owner, NSError *error) {
user.owner = owner;
dispatch_group_leave(self.usersGroup);
}];
}
dispatch_group_notify(self.usersGroup, dispatch_get_main_queue(), ^{
// All requests have finished
// Stuff with updated userlist
});
The main issue is obvious: when first requests have finished, the user reference it has is not the one that has launched the request, so I don't have the correct association.
Is there any easy and elegant way to solve this issue? Thanks in advance!
As #Paulw11 suggested the loop will not wait for you to execute the block so i'm suggesting you to use recursive function like this instead of for loop.
- (void)getOwnerForObectAtIndex:(NSInteger)index {
if (index < self.userList) {
User *user = [self.userList objectAtIndex:index];
[self.wrapper getUserDataWithId:user.userid completion:^(Owner *owner, NSError *error) {
user.owner = owner;
[self getOwnerForObectAtIndex:index+1];
}];
}
else {
//Now all User have its corresponding owner, Handle array Now.
}
}
Now call this function like this [self getOwnerForObectAtIndex:0];

nested callback hell in objective c

I am now working on an app that works with BLE, Backend server and location. I am facing a problem which I am not sure how to get out of which is what people call "Callback hell". The entire CoreBluetooth framework in iOS is based on a delegate pattern, which until you can use the CBPeripheral has to go to at least 3 callbacks:
DidConnectToPeripheral
DidDiscoverServices
DidDiscoverCharacteristics
But in fact there could be many more, and every action you take with the device will come back as a callback to one of those functions. Now when I want to "Rent" this ble product, I must connect to it, after connecting send a requests to the server and get the user's current location, after that all happens I have to write a value in the bluetooth device and get confirmation. This would not be so difficult, but unfortunately each and every one of those stages is failable, so error handling needs to be added. Not to mention implementing timeout.
I am sure I am not the only one to approach such issues so I looked around and I found 2 things that might help:
the Advanced NSOperations talk in the wwdc 2015, but after trying for 4 days to make it work, it seems like the code is too buggy.
Promisekit but I couldn't find a way to wrap CoreBluetooth.
How are people with even more complicated apps deal with this? in swift or objc.
Some sample problematic code:
-(void)startRentalSessionWithLock:(DORLock *)lock timeOut:(NSTimeInterval)timeout forSuccess:(void (^)(DORRentalSession * session))successBlock failure:(failureBlock_t)failureBlock{
//we set the block to determine what happens
NSAssert(lock.peripheral, #"lock has to have peripheral to connect to");
if (!self.rentalSession) {
self.rentalSession = [[DORRentalSession alloc] initWithLock:nil andSessionDict:#{} active:NO];
}
self.rentalSession.lock = lock;
[self connectToLock:self.rentalSession.lock.peripheral timeOut:timeout completionBlock:^(CBPeripheral *peripheral, NSError *error) {
self.BTConnectionCompleted = nil;
if (!error) {
[[INTULocationManager sharedInstance] requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse timeout:1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) {
if (status == INTULocationStatusSuccess || status == INTULocationStatusTimedOut) {
[self startServerRentalForSessionLockWithUserLocation:currentLocation.coordinate forSuccess:^(DORRentalSession *session) {
if (self.rentalSession.lock.peripheral && self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
[self.rentalSession.lock.peripheral setNotifyValue:YES forCharacteristic:self.rentalSession.lock.charectaristics.sensorCharacteristic];
}else{
//shouldnt come here
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
!self.rentalSession.lock.open ? [self sendUnlockBLECommandToSessionLock] : nil;
if (successBlock) {
successBlock(session);
}
}else{
[self endCurrentRentalSessionWithLocation:self.rentalSession.lock.latLng andPositionAcc:#(1) Success:^(DORRentalSession *session) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:46 userInfo:#{NSLocalizedDescriptionKey:#"Could't connect to lock"}],200);
}
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:45 userInfo:#{NSLocalizedDescriptionKey:#"fatal error"}],200);
}
}];
}
});
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock(error,httpCode);
}
}];
}else{
NSError *gpsError = [self donkeyGPSErrorWithINTULocationStatus:status];
if (failureBlock) {
failureBlock(gpsError,200);
}
}
}];
}else{
if (failureBlock) {
failureBlock(error,200);
}
}
}];
}
To get rid of this nested calls you can use GCD group + serial execution queue:
dispatch_queue_t queue = ddispatch_queue_create("com.example.queue", NULL);
dispatch_group_t group = dispatch_group_create();
// Add a task to the group
dispatch_group_async(group, queue, ^{
// Some asynchronous work
});
// Make dispatch_group_async and dispatch_group_sync calls here
// Callback to be executed when all scheduled tasks are completed.
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
// Do smth when everything has finished
});
// wait for all tasks to complete
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
The other solution based on GCD groups is described here

iOS - How to increase variable in block using cycle

I need to create an entity on the server and then upload few images to the server.
So first block display success creating entity on the server then I starting upload 10 images one by one in cycle, but the app send the notification not after last 10 image was uploaded, so 'i' variable can be 10 even not be a 10 in the order. I am not sure but seems iteration in the block is not right. So I just want to be sure that the 10 images was uploaded and just then invoke sending notification.
So I skip some blocks parameters and failure options, array that I use for getting images to upload and etc. Just think about my blocks as an example that display success invocation after '{'.
// success first block
block1
{
// cycle from 0 to 10
for (NSInteger i = 0; i <=10; i++)
{
// success upload image to the server block
block2
{
// if I did 10 uploads I need to send notification.
if (i == 10)
{
// send notification here when last block returned success....
}
}
}
}
If you're creating a bunch of AFHTTPRequestOperation objects, then the most logical approach would be to create a completion operation and make it dependent upon the request operations:
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
// code to be run when the operations are all done should go here
}];
for (...) {
// create `operation` AFHTTPRequestOperation however you want and add it to some queue
// but just make sure to designate the completion operation dependency
[completionOperation addDependency:operation];
}
// now that all the other operations have been queued, you can now add the completion operation to whatever queue you want
[[NSOperationQueue mainQueue] addOperation:completionOperation];
You can use Dispatch Group as the following.
// success first block
block1
{
dispatch_group_t group = dispatch_group_create();
// cycle from 0 to 10
__block NSUInteger successCount = 0;
for (NSInteger i = 0; i <=10; i++)
{
dispatch_group_enter(group);
// upload image to the server block
successOrErrorBlock
{
if (success)
successCount++;
dispatch_group_leave(group);
}
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (successCount == 10) {
// send notification here when last block returned success....
}
});
}
The pattern I follow when invoking more than a couple blocks asynchronously is to create a to-do list (array) and perform that list recursively, like this:
// say this is our asynch operation. presume that someParameter fully
// describes the operation, like a file to be uploaded
- (void)performSomeAsynchOperationDefinedBy:(id)someParameter completion:(void (^)(BOOL, NSError *))completion {
// this could wrap any operation, like anything from AFNetworking
}
- (void)doOperationsWithParameters:(NSArray *)parameters completion:(void (^)(BOOL, NSError *))completion {
if (!parameters.count) return completion(YES, nil);
id nextParameter = someParameters[0];
NSArray *remainingParameters = [parameters subarrayWithRange:NSMakeRange(1, parameters.count-1)];
[self performSomeAsynchOperationDefinedBy:nextParameter completion:^(BOOL success, NSError *error)) {
if (!error) {
[self doManyOperations:remainingParameters completion:completion];
} else {
completion(NO, error);
}
}];
}
Now, to do several operations, place the parameters in an array, like:
NSArray *parameters = #[#"filename0", #"filename1", ...];
[self doOperationsWithParameters:parameters completion:(BOOL success, NSError *error) {
NSLog(#"ta-da!");
}];
A variant on the above with a progress block invoked up front and after each recursion (with a progress percentage initialCount/remainingCount) should be straight forward from the example provided.
Another variation would be to wrap both the param array and the completion block in a single NSObject, so the recursive call could be made with performSelector:, the benefit of doing this would be to not wind up the stack on a long recursion.

downloading data from several pffile's at once asynchronosly

If I have an array of Message objects, each with a PFile containing data, is it possible to download the data for every single message by queuing them up asynchronously like so:
for (int i = 0; i < _downloadedMessages.count; i++) {
PFObject *tempMessage = (PFObject *)[_downloadedMessages objectAtIndex:i];
[[tempMessage objectForKey:#"audio"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[self persistNewMessageWithData:data];
}];
}
This seems to cause my app to hang, even though this should be done in the background...
Using the solution below:
NSMutableArray* Objects = ...
[self forEachPFFileInArray:Objects retrieveDataWithCompletion:^BOOL(NSData* data, NSError*error){
if (data) {
PFObject *tempObj = (PFObject *)Object[someIndex...];
[self persistNewMessageWithData:data andOtherInformationFromObject:tempObj];
return YES;
}
else {
NSLog(#"Error: %#", error);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];
What you are possibly experiencing is, that for a large numbers of asynchronous requests which run concurrently, the system can choke due to memory pressure and due to network stalls or other accesses of resources that get exhausted (including CPU).
You can verify the occurrence of memory pressure using Instruments with the "Allocations" tool.
Internally (that is, in the Parse library and the system) there might be a variable set which sets the maximum number of network requests which can run concurrently. Nonetheless, in your for loop you enqueue ALL requests.
Depending of what enqueuing a request means in your case, this procedure isn't free at all. It may cost a significant amount of memory. In the worst case, the network request will be enqueued by the system, but the underlying network stack executes only a maximum number of concurrent requests. The other enqueued but pending requests hang there and wait for execution, while their network timeout is already running. This may lead to cancellation of pending events, since their timeout expired.
The simplest Solution
Well, the most obvious approach solving the above issues would be one which simply serializes all tasks. That is, it only starts the next asynchronous task when the previous has been finished (including the code in your completion handler). One can accomplish this using an asynchronous pattern which I name "asynchronous loop":
The "asynchronous loop" is asynchronous, and thus has a completion handler, which gets called when all iterations are finished.
typedef void (^loop_completion_handler_t)(id result);
typedef BOOL (^task_completion_t)(PFObject* object, NSData* data, NSError* error);
- (void) forEachObjectInArray:(NSMutableArray*) array
retrieveDataWithCompletion:(task_completion_t)taskCompletionHandler
completion:(loop_completion_handler_t)completionHandler
{
// first, check termination condition:
if ([array count] == 0) {
if (completionHandler) {
completionHandler(#"Finished");
}
return;
}
// handle current item:
PFObject* object = array[0];
[array removeObjectAtIndex:0];
PFFile* file = [object objectForKey:#"audio"];
if (file==nil) {
if (taskCompletionHandler) {
NSDictionary* userInfo = #{NSLocalizedFailureReasonErrorKey: #"file object is nil"}
NSError* error = [[NSError alloc] initWithDomain:#"RetrieveObject"
code:-1
userInfo:userInfo];
if (taskCompletionHandler(object, nil, error)) {
// dispatch asynchronously, thus invoking itself is not a recursion
dispatch_async(dispatch_get_global(0,0), ^{
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
});
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}
}
else {
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
BOOL doContinue = YES;
if (taskCompletionHandler) {
doContinue = taskCompletionHandler(object, data, error);
}
if (doContinue) {
// invoke itself (note this is not a recursion")
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}];
}
}
Usage:
// Create a mutable array
NSMutableArray* objects = [_downloadedMessages mutableCopy];
[self forEachObjectInArray:objects
retrieveDataWithCompletion:^BOOL(PFObject* object, NSData* data, NSError* error){
if (error == nil) {
[self persistNewMessageWithData:data andOtherInformationFromObject:object];
return YES;
}
else {
NSLog(#"Error %#\nfor PFObject %# with data: %#", error, object, data);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];

Resources