Don't trigger RACSignal side effects until it has completed - ios

I'm trying to create signal that will trigger it side effects on first subscription, and replay events for any further subscriptions. Once the signal sends complete or error I would like to re trigger side effect for next subscription. I've come up with this as solution, but I'm wondering if there is more elegant way to solve this.
#interface ViewController ()
#property (nonatomic, assign) NSUInteger counter;
#property (nonatomic, strong) RACSignal *defferedIncrement;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[self defferedIncrement] subscribeNext:^(id x) {
NSLog(#"Count: %#", x);
}];
[[self defferedIncrement] subscribeNext:^(id x) {
NSLog(#"Count: %#", x);
}];
}
- (IBAction)buttonDidTap:(id)sender {
[[self defferedIncrement] subscribeNext:^(id x) {
NSLog(#"Count: %#", x);
}];
}
- (RACSignal *)realIncrement
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACDisposable *disposable = [RACDisposable new];
if (disposable.disposed) { return disposable; }
++self.counter;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:#(self.counter)];
[subscriber sendCompleted];
});
return disposable;
}];
}
- (RACSignal *)defferedIncrement
{
if (!_defferedIncrement) {
_defferedIncrement = [[RACSignal defer:^RACSignal *{
return [[self realIncrement] finally:^{
_defferedIncrement = nil;
}];
}] replayLazily];
}
return _defferedIncrement;
}
#end

Related

ReactiveCocoa after catch the error, the button signal not get triggered again

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.

How to return a RACSignal without using [RACSignal createSignal]

For example, My current implementation is like below:
- (RACSignal *)getPlaylist {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[[buttonClickSignal
flattenMap:^(UIButton *sender) {
return [self logInWithUsername:username password:password];
}]
flattenMap:^(NSDictionary *json) {
return [self fetchPlaylistForToken:token];
}]
subscribeNext:^(NSDictionary *json) {
[subscriber sendNext:json];
[subscriber sendCompleted];
}];
return nil;
}];
}
How to return a new signal without using [RACSignal createSignal] method?
Why don't you just return the mapped buttonClickSignal?
I don't see any problems with just this:
- (RACSignal *)getPlaylist {
return [[buttonClickSignal
flattenMap:^(UIButton *sender) {
return [self logInWithUsername:username password:password];
}]
flattenMap:^(NSDictionary *json) {
return [self fetchPlaylistForToken:token];
}];
}
Since you appear to be ignoring errors right now your current implementation will never actually complete if any of the flattenMapped signals error.

How to bind Realm objects changes?

In my project im trying to work via MVVM,
so in VM in .h file
#property (nonatomic, strong) NSArray *cities;
in .m file
- (NSArray *)cities {
return [[GPCity allObjects] valueForKey:#"name"];
}
GPCity is a RLMObject subclass
How to bind this via ReactiveCocoa (i mean see all cities updates/adds/removes) ?
Something like:
RAC(self, cities) = [[GPCity allObjects] map:(GPCity *)city {return city.name;}];
You can wrap Realm change notifications in a RAC signal:
#interface RLMResults (RACSupport)
- (RACSignal *)gp_signal;
#end
#implementation RLMResults (RACSupport)
- (RACSignal *)gp_signal {
return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
id token = [self.realm addNotificationBlock:^(NSString *notification, RLMRealm *realm) {
if (notification == RLMRealmDidChangeNotification) {
[subscriber sendNext:self];
}
}];
return [RACDisposable disposableWithBlock:^{
[self.realm removeNotification:token];
}];
}];
}
#end
and then do:
RAC(self, cities) = [[[RLMObject allObjects] gp_signal]
map:^(RLMResults<GPCity *> *cities) { return [cities valueForKey:#"name"]; }];
This will unfortunately update the signal after every write transaction, and not just ones which modify cities. Once Realm 0.98 is released with support for per-RLMResults notifications, you'll be able to do the following, which will only update when a GPCity object is updated:
#interface RLMResults (RACSupport)
- (RACSignal *)gp_signal;
#end
#implementation RLMResults (RACSupport)
- (RACSignal *)gp_signal {
return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
id token = [self addNotificationBlock:^(RLMResults *results, NSError *error) {
if (error) {
[subscriber sendError:error];
}
else {
[subscriber sendNext:results];
}
}];
return [RACDisposable disposableWithBlock:^{
[token stop];
}];
}];
}
#end

Filter function is not working in this case?

I am try to follow tutorial regarding to ReactiveCocoa from Ray, but somehow the filter function is working since it always goes down to the subscribeNext although I debugged that filter function does go with the return #NO branch.
#import <Accounts/Accounts.h>
#import <Social/Social.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SearchViewController.h"
typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
RWTwitterInstantErrorAccessDenied,
RWTwitterInstantErrorNoTwitterAccounts,
RWTwitterInstantErrorInvalidResponse
};
static NSString * const RWTwitterInstantDomain = #"TwitterInstant";
#interface SearchViewController ()
{
RACDisposable *requestTwiiterSubscription;
}
#property (strong, nonatomic) ACAccountStore *accountStore;
#property (strong, nonatomic) ACAccountType *twitterAccountType;
#property (weak, nonatomic) IBOutlet UISearchBar *searchBar;
#property (strong, nonatomic) UITextField *searchBarTextField;
#end
#implementation SearchViewController
- (UITextField *)searchBarTextField {
if (!_searchBarTextField) {
for (UIView *view in self.searchBar.subviews) {
for (id deeperView in view.subviews) {
if ([deeperView isKindOfClass:[UITextField class]]) {
_searchBarTextField = deeperView;
}
}
}
}
return _searchBarTextField;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.searchBar.text = #"Co";
__weak SearchViewController *weakSelf = self;
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore
accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
/**
* The then method waits until a completed event is emitted, then subscribes to the signal returned by its block parameter.
* This effectively passes control from one signal to the next.
*/
requestTwiiterSubscription = [[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
return weakSelf.searchBarTextField.rac_textSignal;
}]
filter:^BOOL(NSString *textString) {
if (textString.length >= 3) {
return #YES;
}
return #NO;
}]
subscribeNext:^(id x) {
NSLog(#"%#", x);
} error:^(NSError *error) {
NSLog(#"An error occurred: %#", error);
}];
}
- (void)dealloc {
[requestTwiiterSubscription dispose];
}
- (RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorAccessDenied
userInfo:nil];
// 2 - create the signal
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3 - request access to twitter
[self.accountStore
requestAccessToAccountsWithType:self.twitterAccountType
options:nil
completion:^(BOOL granted, NSError *error) {
// 4 - handle the response
if (!granted) {
[subscriber sendError:accessError];
} else {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
}];
return nil;
}];
}
#end
You have the wrong return values in the filter. You want:
requestTwiiterSubscription = [[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
return weakSelf.searchBarTextField.rac_textSignal;
}]
filter:^BOOL(NSString *textString) {
if (textString.length >= 3) {
return YES;
}
return NO;
}]
subscribeNext:^(id x) {
NSLog(#"%#", x);
} error:^(NSError *error) {
NSLog(#"An error occurred: %#", error);
}];
You were returning NSNumber objects instead of BOOL values.

ReactiveCocoa and delegates

I'm trying to communicate with the login service and update the UI the reactive way. The thing is that my login service works with delegates and almost every example I find works with blocks.
I wrote a solution that works, but it seems a bit to clunky, I'm not sure if this is the best way:
LoginViewController:
- (void) viewDidLoad
{
[super viewDidLoad];
//Assign the "loginCommand" command to the button. It'll get executed on button pressed and the button is only enabled when the command says so.
self.entrarBtn.rac_command = self.viewModel.loginCommand;
//Subscribe and respond to command's successful signals
#weakify(self);
[self.viewModel.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[loginSignal subscribeNext:^(id x) {
#strongify(self);
[self.viewPresenter enterMainNavigation];
}];
}];
//Subscribe and respond to command's error signals
[self.viewModel.loginCommand.errors
subscribeNext:^(NSError* error) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"ERROR" message:[NSString stringWithFormat:#"Error: %#", error.localizedDescription] delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
}];
}
LoginViewModel:
- (id)init
{
self = [super init];
if(self) {
self.loginCommand = [[RACCommand alloc] initWithEnabled:self.enableLoginSignal
signalBlock:^RACSignal *(id input) {
return [self loginSignal];
}];
}
return self;
}
- (RACSignal *)loginSignal
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//LOGIN OK
RACDisposable* loginOKDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginOK)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
PositionGlobalService *positionGlobalService = [PositionGlobalService sharedInstance];
positionGlobalService.delegate = self;
[positionGlobalService getPositionGlobal];
}];
//GETTING USER INFO DELEGATE THEN SEND THE COMPLETED SIGNAL
RACDisposable* positionOKDisposable = [[self rac_signalForSelector:#selector(positionGlobalServiceDidReceivePositionGlobal)
fromProtocol:#protocol(PositionGlobalServiceDelegate)] subscribeNext:^(id x) {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}];
RACDisposable* positionErrorDisposable = [[self rac_signalForSelector:#selector(positionGlobalServiceDidReceivePositionGlobalError:)
fromProtocol:#protocol(PositionGlobalServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorGettingUserInfo userInfo:nil];
[subscriber sendError:error];
}];
//ERRORS
RACDisposable* loginKODisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKO)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorKO userInfo:nil];
[subscriber sendError:error];
}];
RACDisposable* deniedDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKOAccessDenied)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorAccessDenied userInfo:nil];
[subscriber sendError:error];
}];
RACDisposable* connectionErrorDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveConnectionError)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorConnectionError userInfo:nil];
[subscriber sendError:error];
}];
RACDisposable* genericErrorDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveGenericError:)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorGenericError userInfo:nil];
[subscriber sendError:error];
}];
LoginService *loginService = [LoginService sharedInstance];
loginService.delegate = self;
[loginService checkLogin:self.usuario withPassword:self.password documentType:LoginDocumentTypeNIF saveLogin:YES];
return [RACDisposable disposableWithBlock:^{
[loginOKDisposable dispose];
[positionOKDisposable dispose];
[positionErrorDisposable dispose];
[loginKODisposable dispose];
[deniedDisposable dispose];
[connectionErrorDisposable dispose];
[genericErrorDisposable dispose];
}];
}];
}
As you can see there's a bunch of code that is almost the same for every delegate, that's why I'm unsure whether this is the best way to do it.
Your view looks good, but I have a few suggestions for the model. The main point is that I'd simplify the signals on the LoginService and PositionGlobalService by moving them into the respective classes for those services. You can then merge the errors and create a single signal, e.g.:
#interface LoginService : SomeSuperclass<LoginServiceDelegate>
- (RACSignal *)loginWithID:(NSString *)userid password:(NSString *password);
#end
#implementation LoginService()
- (RACSignal *)loginWithID:(NSString *)userid password:(NSString *)password {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACDisposable *errorDisposable = [[RACSignal merge:#[[[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKO) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorKO userInfo:nil]],
[[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKOAccessDenied) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorAccessDenied userInfo:nil]],
[[self rac_signalForSelector:#selector(loginServiceDidReceiveConnectionError) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorConnectionError userInfo:nil]],
[[self rac_signalForSelector:#selector(loginServiceDidReceiveGenericError) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorGenericError userInfo:nil]]]] subscribeNext:^(id x) {
[subscriber sendError:x];
}];
RACDisposable *loginDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginOK) fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
[self checkLogin:userid withPassword:password documentType:LoginDocumentTypeNIF saveLogin:YES];
return [RACDisposable disposableWithBlock:^{
[errorDisposable dispose];
[loginDisposable dispose];
}];
}
}
#end
Then, your login function can become something like this (though I'd probably rename this function since it does two things):
- (RACSignal *)loginSignal
{
return [[[LoginService sharedInstance] loginWithID:self.usuario password:self.password] then:^RACSignal *{
return [[PositionGlobalService sharedInstance] getPositionGlobalSignal];
}];
}];

Resources