Is there any way to cancel execution of a RACCommand?
For example I have a command with infinite execution signal like this:
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
__block BOOL stop = NO;
while (!stop) {
[subscriber sendNext:nil];
}
return [RACDisposable disposableWithBlock:^{
stop = YES;
}];
}];
}];
So how can I stop it after calling [command execute:nil]?
I'm a bit new to RACCommand, so I'm not certain there's a better way to do this. But I have been using takeUntil: with a cancellation signal to halt execution.
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
while (true) {
[subscriber sendNext:nil];
}
}] takeUntil:cancellationSignal];
}];
Related
So, when I trying to fetch some data, RACCommand return this error.
I have a picker for example and when user scroll it, app get data from server and show them, but if user scroll fast, (previous operation in progress) RACCommand get this error:
Error Domain=RACCommandErrorDomain Code=1 "The command is disabled and cannot be executed" UserInfo={RACUnderlyingCommandErrorKey=<RACCommand: 0x174280050>, NSLocalizedDescription=The command is disabled and cannot be executed}
I know, its related with some cancel mechanism, but I tried many examples and not working as well.
Its my piece of code:
#weakify(self);
[[[self.viewModel makeCommand] execute:nil]
subscribeError:^(NSError *error) {
#strongify(self);
[self showAlertWithError:error];
}];
and viewModel:
- (RACCommand*)makeCommand {
if (!_makeCommand) {
_makeCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [self getVehicleMake];
}];
}
return _makeCommand;
}
- (RACSignal*)getVehicleMake {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[self.forumService getForumMakesWithYear:#([self.selectedYear integerValue])
category:self.vehicleCategory]
subscribeNext:^(RACTuple *result) {
self.makes = result.first;
[subscriber sendNext:self.makes];
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
}];
}];
}
RACCommand doesn't allow concurrent execution by default. When it's executing, it becomes disabled. If you try to execute again, it will send that error.
But you can test for that error—RACCommand has RACCommandErrorDomain and RACCommandErrorNotEnabled constants available.
#weakify(self);
[[[self.viewModel makeCommand] execute:nil]
subscribeError:^(NSError *error) {
#strongify(self);
if ([error.domain isEqual:RACCommandErrorDomain] && error.code == RACCommandErrorNotEnabled) {
return;
}
[self showAlertWithError:error];
}];
I have the following code which is invoked by another signal:
- (RACSignal *)uploadFormItemAttachment_SO:(MyManagedAttachment *)attachment attachmentHeader:(MyFormItemAttachmentHeader*) attachmentHeader
{
RACSignal* signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// Code omitted for brevity
// NSDictionary* parameters = #{ #"parameter1" : param1_value};
AFHTTPRequestOperation* operation = [self.requestOperationManager POST:#"UploadForm" parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:fileURL
name:#"content"
error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self logRequest:operation response:responseObject];
BOOL success = [responseObject[#"success"] boolValue];
if (success)
{
attachment.status = #(AttachmentStatusSent);
[attachment.managedObjectContext MR_saveToPersistentStoreAndWait];
[subscriber sendNext:#(success)];
[subscriber sendCompleted];
}
else
{
NSDictionary* userInfo = nil;
NSString* message = responseObject[#"message"];
if (message != nil && ![message isEqual:[NSNull null]])
{
userInfo = #{ NSLocalizedDescriptionKey : message };
}
else
{
userInfo = #{ NSLocalizedDescriptionKey : [NSString stringWithFormat:#"Error uploading image: %#", filename]};
}
NSError* error = [NSError errorWithDomain:SEFSAPIClientErrorDomain code:1100 userInfo:userInfo];
[subscriber sendError:error];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self logRequest:operation error:error];
[subscriber sendError:error];
}];
[self.requestOperationManager.operationQueue addOperation:operation];
return [RACDisposable disposableWithBlock:^{
[operation cancel];
}];
}];
return signal;
}
My problem is that the signal object is never invoked and the code never runs. It goes straight into the last statement
return signal;
and the code inside the signal never gets executed. Could anyone help please?
I have tried adding a [subscriber sendNext: signal] at the end of the RACSignal definition but it makes no difference.
The previous method is called by this one below:
- (RACSignal *)uploadPendingAttachments_SO
{
NSArray *pendingAttachments = [MyManagedAttachment MR_findAllWithPredicate:[NSPredicate predicateWithFormat:#"status == %ld && form.status == %ld", MyAttachmentStatusPending, SEFSFormStatusUploadedForm]
inContext:[NSManagedObjectContext MR_defaultContext]];
RACSignal* batchSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[pendingAttachments enumerateObjectsUsingBlock:^(MyManagedAttachment* pendingImage, NSUInteger idx, BOOL *stop) {
// Check if we have a top list item id which indicates form data has been created on the back-end SharePoint server.
NSString* siteID = pendingImage.siteID;
NSString* formID = pendingImage.formID;
NSManagedObjectContext* context = [NSManagedObjectContext MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"projectID = %# AND formID = %#", siteID, formID];
MyManagedForm* form = [MyManagedForm MR_findFirstWithPredicate:predicate
inContext:context];
if (form.topListItemID && pendingImage.formItemID) {
NSString* listName = pendingImage.list;
RACSignal* getFormItemAttachmentHeadersSignal = [[self getFormItemAttachmentHeaders:listName
topListItemID:form.topListItemID
form:form
] map:^id(NSMutableArray* value) {
NSArray* attachmentHeaders = [value copy];
RACSignal* attachmentsBatchSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[attachmentHeaders enumerateObjectsUsingBlock:^(MyFormItemAttachmentHeader* attachmentHeader, NSUInteger idx, BOOL *stop)
{
// Look for the local attachment using attachment header from server
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"identifier = %#", attachmentHeader.document];
NSArray* foundAttachment = [pendingAttachments filteredArrayUsingPredicate:predicate];
MyManagedAttachment* fullAttachment = foundAttachment[0];
NSString* listName = pendingImage.list;
RACSignal* uploadFormItemAttachmentSignal = [[self uploadFormItemAttachment_SO:fullAttachment
attachmentHeader:attachmentHeader ] map:^id(id value) {
BOOL success = [value boolValue];
return nil;
}];
[subscriber sendNext:uploadFormItemAttachmentSignal];
}];
[subscriber sendCompleted];
return nil;
}];
return attachmentsBatchSignal; // check this
}];
[subscriber sendNext:getFormItemAttachmentHeadersSignal];
}
else {
RACSignal* uploadFormSignal = [[self uploadAttachment:pendingImage] map:^id(NSNumber* value) {
NSMutableArray* valuesArray = [NSMutableArray array];
[valuesArray addObject:value];
[valuesArray addObject:pendingImage.objectID];
RACTuple* tuple = [RACTuple tupleWithObjectsFromArray:valuesArray
convertNullsToNils:YES];
return tuple;
}];
[subscriber sendNext:uploadFormSignal];
}
}];
[subscriber sendCompleted];
return nil;
}];
return [batchSignal flatten:2];
}
I have 3 dependent signals and I want to combine their values into a single object. I came up with 2 options.
Option 1:
+ (RACSignal *)createObject {
RACSignal *paramsSignal = [[[self class] createObject1] flattenMap:^RACStream *(NSString *object1) {
return [[[self class] createObject2:object1] flattenMap:^RACStream *(NSString *object2) {
return [[[self class] createObject3:object2] flattenMap:^RACStream *(NSString *object3) {
return [RACSignal return:RACTuplePack(object1, object2, object3)];
}];
}];
}];
return [paramsSignal map:^id(RACTuple *tuple) {
return [[CombinedObject alloc] initWithO1: tuple.first O2: tuple.second O3: tuple.third];
}];
}
I don't quite like all that nesting and closures.
Option 2:
+ (RACSignal *)createObject {
RACSignal *paramsSignal = [[[[self class] createObject1] flattenMap:^id(NSManagedObjectModel *object1) {
return [RACSignal combineLatest:#[[RACSignal return:object1], [[self class] createObject2:object1]]];
}] flattenMap:^RACStream *(RACTuple *tuple) {
return [RACSignal combineLatest:#[[RACSignal return:tuple.first], [RACSignal return:tuple.second], [[self class] createObject3:tuple.second]]];
}];
return [paramsSignal map:^id(RACTuple *tuple) {
return [[CombinedObject alloc] initWithO1: tuple.first O2: tuple.second O3: tuple.third];
}];
}
No nesting or closures but too much of tuples and objects are passing through each signal...
So I was wondering if there is more elegant solution I'm missing exist.
Thanks.
How about stack-like approach?
- (NSArray *) createPhases {
return #[#selector(createObject1:), #selector(createObject2:), #selector(createObject3:)];
}
...
+ (RACSignal *)createObject {
NSMutableArray *stack = [NSMutableArray new];
for (SEL phase: [self createPhases]) {
stack = [[[self class] performSelector:phase withObject:stack] flattenMap:^RACStream *(NSString *append) {
return [stack arrayByAddingArray:#[append]]; // or with some other manipulations
}];
}
return [self withObjects:stack map:^id(NSArray *stack) {
return [[CombinedObject alloc] initWithObjects:stack];
}];
}
- (...) createObject1:(NSArray *)chain {
// here chain will be empty, actually (#[])
return ... // create object 1
}
- (...) createObject2:(NSArray *)chain {
// here chain will be #[object1]
return [... ... withObject1:[chain lastObject]]; // create object 2
}
- (...) createObject3:(NSArray *)chain {
// here chain will be #[object1, object2]
return [... ... withObject2:[chain lastObject]]; // create object 3
}
I tried to use ReactiveCocoa to do network operation chaining, but I failed. I can't figure out what is wrong with my code.
- (RACSignal *)pg_findObjectsInBackground {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:objects];
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
[self cancel];
}];
}];
}
- (RACSignal *)pg_countObjectsInBackground {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
if (error) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:#(number)];
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
[self cancel];
}];
}];
}
__block NSError *_error;
#weakify(self)
[[[self.query pg_countObjectsInBackground]flattenMap:^RACStream *(NSNumber *count) {
#strongify(self)
self.totalCount = [count integerValue];
// Second, fetch experiences
self.query.limit = self.pageSize;
self.query.skip = self.pageSize * self.currentPage;
return [self.query pg_findObjectsInBackground];
}]subscribeNext:^(NSArray *experiences) {
#strongify(self)
[self.experiences removeAllObjects];
[self.experiences addObjectsFromArray:experiences];
} error:^(NSError *error) {
_error = error;
} completed:^{
#strongify(self)
if (finishBlock) {
finishBlock(self, _error);
}
}];
The first request was successful. But as soon as I return [self.query pg_findObjectsInBackground], it went to disposableWithBlock directly.
Because you're using the same PFQuery object for both the count and the find object operation, the query gets canceled when you return from the flattenMap method. The flattenMap subscribes to the new signal (which is the same signal), which I believe causes the disposable to fire. A simple solution is to construct a new PFQuery and return it in the flattenMap block.
I assumed you're using Parse, and if you are, you should tag it.
I have 2 performance test methods in test class. If i run them separately they pass. If i run hole class methods they fail with message:
**** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'API violation - multiple calls made to -[XCTestExpectation fulfill].'*
Is there any way to include couple performance tests in 1 class?
here is sample code:
- (void)testPerformanceAuthenticateWithLogin {
XCTestExpectation *authenticateExpectation = [self expectationWithDescription:#"Authenticate With Login"];
__block int userID = 0;
[self measureBlock:^{
[AuthenticationService authenticateWithLogin:email password:password success:^(AuthenticationResponse *result) {
XCTAssert(result.isAuthenticated);
userID = result.userID;
[authenticateExpectation fulfill];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
}];
[self waitForExpectationsWithTimeout:3 handler:^(NSError *error) {
XCTAssertNil(error);
[AuthenticationService logoutWithServicePersonID:userID success:nil failure:nil];
}];
}
- (void)testPerformanceGetServicePersonByID {
XCTestExpectation *getServicePersonExpectation = [self expectationWithDescription:#"get Service Person By ID"];
__block int userID = 0;
[AuthenticationService authenticateWithLogin:email password:password success:^(AuthenticationResponse *result) {
userID = result.userID;
[self loginSuccess:result];
[self measureBlock:^{
[ServicePersonService getServicePersonByIDWithServicePersonID:userID success:^(ServicePersonDTO *result) {
XCTAssertNotNil(result);
[getServicePersonExpectation fulfill];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
}];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
[self waitForExpectationsWithTimeout:3 handler:^(NSError *error) {
XCTAssertNil(error);
[AuthenticationService logoutWithServicePersonID:userID success:nil failure:nil];
}];
}
The measureBlock actually runs the code inside the block multiple times to determine an average value.
If I understand correctly you want to measure how long does the authentication take. If that's the case:
you could put the expectation inside the measure block so every time a measure iteration is done, the test iteration will wait for the expectation to be fulfilled before executing the block again
you could bump up the expectation timeout a bit just in case.
I've done something like this (which worked for me):
- (void)testPerformanceAuthenticateWithLogin {
[self measureBlock:^{
__block int userID = 0;
XCTestExpectation *authenticateExpectation = [self expectationWithDescription:#"Authenticate With Login"];
[AuthenticationService authenticateWithLogin:email password:password success:^(AuthenticationResponse *result) {
XCTAssert(result.isAuthenticated);
userID = result.userID;
[authenticateExpectation fulfill];
} failure:^(NSError *error) {
XCTAssertNil(error);
}];
[self waitForExpectationsWithTimeout:10 handler:^(NSError *error) {
XCTAssertNil(error);
[AuthenticationService logoutWithServicePersonID:userID success:nil failure:nil];
}];
}];
}