RACSignal *s1 = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(#"s1");
[subscriber sendCompleted];
});
return nil;
}];
RACSignal *s2 = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(#"s2");
[subscriber sendCompleted];
});
return nil;
}];
self.command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [s1 then:^RACSignal * _Nonnull{
return s2;
}];
}];
[self.command execute:nil];
[self.command.executionSignals subscribeCompleted:^{
NSLog(#"completed");
}];
I found that completed did not excute. How to make it work?
RACCommand.executionSignals is one signal that belongs to the RACCommand instance.
On this signal, the command will send another RACSignal as value for each execution of the command.
With your snippet
[self.command.executionSignals subscribeCompleted:^{
NSLog(#"Command was deallocated");
}];
you are observing the RACSignal<RACSignal> of the command - this signal will only complete, when the RACSignal is deallocated.
If you really need to observe the command, you will need to do the following:
[self.command.executionSignals subscribeNext:^(RACSignal *innerSignal) {
[innerSignal subscribeCompleted:^{
NSLog(#"One execution of the command completed");
}];
}];
This subscribeNext will be called with each execute of the RACCommand and the innerSignal is the signal you want to observe.
However, if you actually want to know when a single execution is completed, you can subscribe directly to the signal thats returned from the execute call:
[[self.command execute:nil] subscribeCompleted:^{
NSLog(#"This execution of the command completed");
}];
Related
-(void)init{
self.sema = dispatch_semaphore_create(1)
}
-(void)main{
//sending one message is fine
[self preSendMessage:#"hi"];
//deadlock happen when sending multiple msg in a short time.
for(int i = 0; i < 10; i++){
[self preSendMessage:#"yo"];
}
}
- (void)preSendMessage:(NSString*)msg
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER);
RACSignal* signal = [self sendMessage:msg];
#weakify(self);
[signal subscribeNext:^(id x) {
} error:^(NSError *error) {
#strongify(self);
dispatch_semaphore_signal(self.sema);
} completed:^{
#strongify(self);
dispatch_semaphore_signal(self.sema);
}];
});
}
Some informations:
In sendMessage function, I am using AFNetworking.
When I remove the semaphore logic, it works fine, I need this because need to control the sequence of sending msgs.
[Update] my semaphore is init with value 1, so It would run at the first time
My Target is to ensure the code inside preSendMessage execute when the previous one is completed/error
Edit: you should use dispatch_semaphore_create(0)
You should put dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER); after dispatch_async(....)
Something like this:
- (void)preSendMessage:(NSString*)msg{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
RACSignal* signal = [self sendMessage:msg];
#weakify(self);
[signal subscribeNext:^(id x) {
} error:^(NSError *error) {
#strongify(self);
dispatch_semaphore_signal(self.sema);
} completed:^{
#strongify(self);
dispatch_semaphore_signal(self.sema);
}];
});
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER);
}
You are calling dispatch_semaphore_wait in the wrong order. Your code subscription does not get executed, because your semaphore is in a wait state. You should call your dispatch_semaphore_wait right after dispatching to background thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
RACSignal* signal = [self sendMessage:msg];
#weakify(self);
[signal subscribeNext:^(id x) {
} error:^(NSError *error) {
#strongify(self);
dispatch_semaphore_signal(self.sema);
} completed:^{
#strongify(self);
dispatch_semaphore_signal(self.sema);
}];
});
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER);
You tell your app to wait for the semaphore, so your app waits for the semaphore. It doesn't execute any code after the wait call.
You need to set up everything, including the callbacks that will signal, and then you wait for the signal.
If I add a UIControlEventTouchUpInside signal to a doneButton, and call an API, if the API fails, the catch will be called. But if I try to click the button again, the button control event does not get triggered.
- (void)viewDidLoad {
[super viewDidLoad];
[[[[[self.doneButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) {
[SVProgressHUD show];
}] flattenMap:^RACStream *(id value) {
return [[HttpService sharedService] updateImageData:UIImageJPEGRepresentation(self.signatureImageView.image, 0.5)];
}] catch:^RACSignal *(NSError *error) {
[SVProgressHUD showErrorWithStatus:error.localizedDescription];
return [RACSignal empty];
}] subscribeNext:^(id x) {
[SVProgressHUD dismiss];
[self.navigationController popToRootViewControllerAnimated:YES];
}];
}
I think this thread will help. https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1218
A signal will automatically be unsubscribed to if it fails / errors. You can use - retry, however that will simply keep trying your operation until is doesn't fail, which, if there is a perpetual issue will just loop indefinitely.
Wrapping this condition in a flattenMap will capture the issue without unsubscribing the initial rac_signalForControlEvents observation.
See mdieps comment in the thread above on GitHub, and maybe do something like.
[[[[self.doneButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) {
[SVProgressHUD show];
}] flattenMap:^RACStream *(id value) {
return [[[HttpService sharedService] updateImageData:UIImageJPEGRepresentation(self.signatureImageView.image, 0.5)]
catch:^RACSignal *(NSError *error) {
[SVProgressHUD showErrorWithStatus:error.localizedDescription];
return [RACSignal empty];
}];
}] subscribeNext:^(id x) {
[SVProgressHUD dismiss];
[self.navigationController popToRootViewControllerAnimated:YES];
}];
I've not actually constructed a test with this code. Just guessing based on what you might have in your HttpService Class.
You can use RACCommand to solve this problem.
RACCommand *doneCommand =
[[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString *selected) {
return [[[self updateImageSignal]
doCompleted:^{
[SVProgressHUD dismiss];
[self.navigationController popToRootViewControllerAnimated:YES];
}] doError:^(NSError *error) {
[SVProgressHUD showErrorWithStatus:error.localizedDescription];
}];
}];
self.doneButton.rac_command = doneCommand;
Now create RACSignal that send success and error according to your request.
-(RACSignal *)updateImageSignal {
#weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
#strongify(self)
[[HttpService sharedService] updateImageData:UIImageJPEGRepresentation(self.signatureImageView.image, 0.5)
complete:^(BOOL success) {
if(success)
[subscriber sendNext:#(success)];
else
[subscriber sendError:nil];
[subscriber sendCompleted];
}];
return nil;
}];
}
Hope it will help you. And If you have any question then feel free to ask.
I am using XCTestExpectation in a lot of tests and sometimes (very randomly) some expectations are not fulfilled (although I am sure they should be).
While investigating this problem I have noticed that some expectations are fulfilled in a main thread and some are fulfilled in a background thread. And so far these problems are with the ones fulfilled in a background thread.
Is it safe to fulfill expectations from a background thread? I could not find any explicit information about that.
Below is an example of how I use XCTestExpectation:
__block XCTestExpectation *expectation = [self expectationWithDescription:#"test"];
[self doSomethingAsyncInBackgroundWithSuccess:^{
[expectation fullfill];
}];
[self waitForExpectationsWithTimeout:10.0 handler:^(NSError *error) {
expectation = nil;
if (error) {
NSLog(#"Timeout Error: %#", error);
}
}];
It's not documented anywhere that XCTestExpectation is thread-safe. due to there being no official documentation on the matter you can only guess by creating test examples:
- (void)testExpectationMainThread;
{
__block XCTestExpectation *expectation = [self expectationWithDescription:#"test"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
- (void)testExpectationStartMainThreadFulfilBackgroundThread;
{
__block XCTestExpectation *expectation = [self expectationWithDescription:#"test"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, kNilOptions), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
- (void)testExpectationBackgroundThread;
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, kNilOptions);
__block XCTestExpectation *expectation;
dispatch_sync(queue, ^{
expectation = [self expectationWithDescription:#"test"];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
Here it does not crash or cause a problem however due to the lack of official documentation it is probably safer to stick to the same queue to fulfil.
you should really be stubbing the method doSomethingAsyncInBackgroundWithSuccess and provide the app with local "dummy" data.
Your unit tests should not rely on network as it is something which is variable.
You should be executing the completion block of doSomethingAsyncInBackgroundWithSuccess on the main thread (or at least provide a way to call back consistently on the same thread), you can easily do this with GCD.
- (void)doSomethingAsyncInBackgroundWithSuccess:(void (^)(void))completion;
{
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
or use NSOperationQueue mainQueue
- (void)doSomethingAsyncInBackgroundWithSuccess:(void (^)(void))completion;
{
[NSOperationQueue.mainQueue addOperationWithBlock:^{
completion();
}];
}
I'm trying to fetch JSON data from 5 different URLs. The network requests can be performed in parallel, though the responses have to be processed in a certain order. In addition, I also want to have a single point of error handling logic.
The code I'm having right now is like the following. The problem is, only the subscription of signalFive and signalSix has been invoked. The subscribeNext block for all the other signals has never been invoked. I suspect the problem is because the subscription happens after the sendNext occurs.
Is there a better/standard way to perform this kind of request?
- (RACSubject *)signalForFetchingFromRemotePath:(NSString *)remotePath
{
RACSubject *signal = [RACSubject subject];
[self.requestOperationManager GET:remotePath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
for (id obj in responseObject) {
[signal sendNext:obj];
}
[signal sendCompleted];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[signal sendError:error];
}];
return signal;
}
FMDatabase *db = [SomeDatabase defaultDatabase];
[db beginTransaction];
RACSubject *singalOne = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_ONE_PATH]];
RACSubject *singalTwo = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_TWO_PATH]];
RACSubject *singalThree = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_THREE_PATH]];
RACSubject *singalFour = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_FOUR_PATH]];
RACSubject *singalFive = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_FIVE_PATH]];
RACSubject *singalSix = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_SIX_PATH]];
RACSignal *combined = [RACSignal merge:#[singalOne, singalTwo, singalThree, singalFour, singalFive, singalSix]];
[combined subscribeError:^(NSError *error){
[db rollback];
}];
[singalFive subscribeNext:^(NSDictionary *dict) {
[ClassE save:dict];
} completed:^{
[singalSix subscribeNext:^(NSDictionary *dict) {
[ClassF save:dict];
} completed:^{
[singalOne subscribeNext:^(NSDictionary *dict){
[ClassA save:dict];
} completed:^{
[singalTwo subscribeNext:^(NSDictionary *dict){
[ClassB save:dict];
} completed:^{
[singalThree subscribeNext:^(NSDictionary *dict) {
[ClassC save:dict];
} completed:^{
[singalFour subscribeNext:^(NSDictionary *dict){
[ClassD save:dict];
} completed:^{
NSLog(#"Completed");
[db commit];
}];
}];
}];
}];
}];
}];
If you need to enforce a specific order, use +concat: instead of +merge:.
On its own, concatenation means that the requests will not be performed in parallel. If you want to recover that behavior, you can use -replay on each signal (to start it immediately) before passing it to +concat:.
As an aside, nested subscriptions are almost always an anti-pattern. There's usually a built-in operator to do what you want instead.
I usually use combineLatest:
NSArray *signals = #[singalOne, singalTwo, singalThree, singalFour, singalFive, singalSix];
[[RACSignal combineLatest:signals] subscribeNext:^(RACTuple *values) {
// All your values are here
} error:^(NSError *error) {
// error
}];
I'm currently calling storeViewController loadProductWithParameters via dispatch_async . Is it possible to set a timeout value so it only tries to fetch the results for X seconds and then gives up?
I implemented my own timeout by using with the class method below instead of calling loadProductWithParameters directly. It times out thanks to a dispatch_after and __block variable.
+ (void)loadProductViewControllerWithTimeout:(NSTimeInterval)timeout
storeKitViewController:(SKStoreProductViewController *)storeKitViewController
parameters:(NSDictionary *)parameters
completionHandler:(void (^)(BOOL result, NSError *error))completionHandler {
__block BOOL hasReturnedOrTimedOut = NO;
[storeKitViewController loadProductWithParameters:parameters completionBlock:^(BOOL result, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!hasReturnedOrTimedOut) {
hasReturnedOrTimedOut = YES;
if (completionHandler) completionHandler(result, error);
}
});
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!hasReturnedOrTimedOut) {
hasReturnedOrTimedOut = YES;
if (completionHandler) completionHandler(NO, nil); // Or add your own error instead of |nil|.
}
});
}
My latest app update got rejected by Apple because loadProductWithParameters never called its completionBlock and stopped my users from buying songs on iTunes... Hope this helps.
I have acomplished it like so:
__block BOOL timeoutOrFinish = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(!timeoutOrFinish) {
timeoutOrFinish = YES;
[self dismissAndShowError];
}
});
[storeViewController loadProductWithParameters:parameters completionBlock:^(BOOL result, NSError * _Nullable error) {
if(timeoutOrFinish) {
return;
}
timeoutOrFinish = YES;
//[[NetworkManager sharedManager] showNetworkActivityIndicator:NO];
if(error) {
[self dismissAndShowError];
}
}];
[self.view.window.rootViewController presentViewController:storeViewController animated:YES completion:nil];
where dismissAndShowError method runs dismissViewControllerAnimated and shows alert with an error.
Basically, you have a separate timer (30 seconds in my case) that switches a flag. After that time, if store has still not been loaded, I close it and display an error. Otherwise, completion is called (on cancel, finish and error) and handles all actions according to the status.