Fairly new to ReactiveCocoa, I'm trying to build a signal that asynchronously fetches some resource from a remote API to which the client has to authenticate first. Authentication is handled by first getting a token from the API, and then passing it via some custom HTTP header for each subsequent request. However, the custom header might be set after the fetchResource signal is subscribed to, which in the current situation leads to an unauthenticated request. I guess I could actually build the request in the subscribeNext block of self.authenticationStatus, thus ensuring that the token will be set, but how could I handle the disposition of the signal then?
- (RACSignal *)fetchResource
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSURLRequest *request = [self.requestSerializer
requestWithMethod:#"GET"
URLString:[[NSURL URLWithString:#"resource" relativeToURL:self.baseURL] absoluteString]
parameters:nil error:nil];
NSURLSessionDataTask *task = [self dataTaskWithRequest:request
completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
[subscriber sendError:error];
} else {
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
}
}];
// Actually trigger the request only once the authentication token has been fetched.
[[self.authenticationStatus ignore:#NO] subscribeNext:^(id _) {
[task resume];
}];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
}
- (RACSignal *)fetchTokenWithCredentials:(Credentials *)credentials
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// Fetch the token and send it to `subscriber`.
Token *t = ... ;
[subscriber sendNext:t];
return nil;
}];
}
- (RACSignal *)fetchResourceWithToken:(Token *)token
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// Use `token` to set the request header. Then fetch
// the resource and send it to `subscriber`. Basically
// this part is what you already have.
Resource *r = ... ;
[subscriber sendNext:r];
return nil;
}];
}
In your view controller, present the modal authentication dialog if you don't have a valid token. When the user taps the "submit" button, do something like the following:
- (IBAction)handleAuthenticationSubmit:(id)sender
{
Credentials *c = ... ;
RACSignal *resourceSignal = [[[self fetchTokenWithCredentials:c]
flattenMap:^(Token *t) {
return [self fetchResourceWithToken:t];
}]
deliverOn:RACScheduler.mainThreadScheduler];
[self rac_liftSelector:#selector(receiveResource:) withSignals:resourceSignal, nil];
}
- (void)receiveResource:(Resource *)resource
{
[self.delegate authenticationController:self didReceiveResource:resource];
}
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 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.
So what I'm trying to do is this.
Make a HTTP request including a header which specifies the authorization token.
If the response is a 401 Unauthorized I wish to use my refresh token to obtain a new access token and a new refresh token.
When I have the new token pair I wish to retry the failed request using the new tokens.
I got this working by looking at this article. They solve it like this.
- (RACSignal *)doRequestAndRefreshTokenIfNecessary:(RACSignal *)requestSignal {
return [requestSignal catch:^(NSError *error) {
// Catch the error, refresh the token, and then do the request again.
BOOL hasRefreshToken = [UserManager sharedInstance].refreshToken != nil;
BOOL httpCode401AccessDenied = error.code == -1011;
if (httpCode401AccessDenied && hasRefreshToken) {
return [[[self refreshToken] ignoreValues] concat:requestSignal];
}
return requestSignal;
}];
}
But here's the catch. If I fire multiple requests with an expired access token I sometimes end up with a race condition where there are multiple refreshes.
So I thought that I could save the refresh signal as an instance variable and just "chain" all subsequent failed requests to that. But the result is that the refresh signal is done multiple times.
- (RACSignal *) refreshToken {
NSDictionary *params = #{
#"client_id": [Config clientId],
#"client_secret": [Config clientSecret],
#"grant_type": #"refresh",
#"refresh_token": [[Tokens sharedTokens] refreshToken]
};
if (self.activeRefreshSignal == nil) {
#weakify(self)
self.activeRefreshSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
#strongify(self)
[[RestAPI sharedClient] POST:#"/oauth/token"
parameters:params
success:^(NSURLSessionDataTask *operation, id responseObject) {
[self extractTokensFromResponseObject:responseObject];
[subscriber sendNext:nil];
[subscriber sendCompleted];
self.activeRefreshSignal = nil;
}
failure:^(NSURLSessionDataTask *operation, NSError *error) {
[[Tokens sharedTokens] clearTokens];
[subscriber sendError:error];
self.activeRefreshSignal = nil;
}];
return nil;
}];
}
return self.activeRefreshSignal;
}
Any ideas how to avoid the race condition and only do one refresh?
AFNetworking code has a few places in which __block is used for objects in methods where there is no obvious need to change the object. For example, In AFHTTPSessionManager, the GET call uses __block on the task object. Any idea why?
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:#"GET" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters];
__block NSURLSessionDataTask *task = [self dataTaskWithRequest:request completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
return task;
}
Similarly in other classes, __block is used for objects, as shown below for credential object.
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
In both of these places, __block is useless and unnecessary.
In the first case, the variable task is not assigned to after initialization on the line where it is defined. __block is only useful if the variable is assigned to (and for non-retaining purposes in MRC), so it is pointless here.
In the second case, the variable credential is not captured in a block at all, so again it is useless.
Short version:
Is it possible to use RACMulticastConnection in the same way as NSNotificationCenter? I mean to keep the subscribed blocks valid even for another call [connection connect]?
Long version:
Among different subscribers I share a reference to RACMulticastConnection. Those who initiate the request pass loginRequest!=nil and those subscribers who want just listen use loginRequest==nil:
RACMulticastConnection *connection = [self.appModel loginRequest:loginRequest];
[connection connect]; //I initiate the request
[connection.signal subscribeNext:^(RACTuple* responseTuple) {
} error:^(NSError *error) {
} completed:^{
}];
In other modul I just subscribe and listen:
RACMulticastConnection *connection = [self.appModel loginRequest:nil];
[connection.signal subscribeNext:^(RACTuple* responseTuple) {
} error:^(NSError *error) {
} completed:^{
}];
When I call the [connection connect]; everything works fine. The subscribers are notified. But if I want to repeat the request to the server again with [connection connect]; I just receive successful signal with the old responses.
Basic idea is I want to create RACMulticastConnection once and share it for potential subscribers. Those who listen pass nil arguments, those who initiate the request pass not nil argument and call [connection connect]; But it does not trigger the block defined in RACSignal createSignal:.
The RACSignal is created just once when RACMulticastConnection does not exist. self.loginRequestConnection is property of model. The property is shared in the application to subscribers:
- (RACMulticastConnection*) loginRequest:(LoginRequest*)request {
self.loginRequest = request;
if(! self.loginRequestConnection) { // the instance of RACMulticastConnection shared among the subscribers
#weakify(self);
RACSignal* networkRequest = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
__block Response* blockResponse = nil;
#strongify(self);
[_serviceClient login:self.loginRequest success:^(TSNApiResponse *response, TSNError *error) {
blockResponse = response;
[subscriber sendNext:RACTuplePack(response, error)];
[subscriber sendCompleted];
} failure:^(TSNError *error) {
[self cleanUpRequestOnError:error subscriber:subscriber blockResponse:blockResponse blockRequest:self.loginRequest];
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[_serviceClient cancelRequest:self.loginRequest];
}];
}];
self.loginRequestConnection = [networkRequest multicast:[RACReplaySubject subject]];
}
return self.loginRequestConnection;
}
Is there any correct way how to make the connection trigger again the block in the RACSignal? Thank you.
I have used FBKVOController. The code done with FBKVOController is fraction of the implementation done with ReactiveCocoa:
- (void) loginRequest:(TSNLoginRequest*)request {
[_serviceClient login:request success:^(TSNApiResponse *response, TSNError *error) {
self.loginResponseTuple = [TSNResponseTuple responseTuple:response error:error];
} failure:^(TSNError *error) {
self.loginResponseTuple = [TSNResponseTuple responseTuple:nil error:error];
}];
}
Issuing the request:
[self.appModel loginRequest:loginRequest];
Observing the response:
[_KVOController observe:self.appModel keyPath:#"loginResponseTuple" options:NSKeyValueObservingOptionNew block:^(TSNStartupScreenViewModel* observer, TSNAppModel* observed, NSDictionary *change) {
TSNResponseTuple* responseTuple = change[NSKeyValueChangeNewKey];
#strongify(self);
if([responseTuple.error isError]) {
[TSNAppUtilities showError:(TSNError *)(responseTuple.error) completion:^(OHAlertView *alert, NSInteger buttonIndex) {
if(buttonIndex == alert.firstOtherButtonIndex) {
// ... process selection
}
}];
}
}];