Side effects delegate reactivation approach

Just trying to wrap my head around ReactiveCocoa's approach to certain situations.

I have a situation where the segment controller swaps the child controllers. I need to do a couple of things here:

  • When moving to the parent controller, I have to update the contentInset tableView because iOS7 does not handle it for me with custom container views
  • When the search is initiated, I need to extinguish the navigation bar and update the contentInset using animation
  • When the search ends, I need to fade the navigationBar back and reset the contentInset in the animation

Here is the current code that implements this in an imperative style:

 - (void)didMoveToParentViewController:(UIViewController *)parent { [super didMoveToParentViewController:parent]; if (parent) { CGFloat top = parent.topLayoutGuide.length; CGFloat bottom = parent.bottomLayoutGuide.length; if (self.tableView.contentInset.top != top) { UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0); self.tableView.contentInset = newInsets; self.tableView.scrollIndicatorInsets = newInsets; } } } - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar { [UIView animateWithDuration:.25 animations:^{ self.navigationController.navigationBar.alpha=0; self.tableView.contentInset = UIEdgeInsetsMake([UIApplication sharedApplication].statusBarFrame.size.height, 0, 0, 0); }]; return YES; } - (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar { [UIView animateWithDuration:.25 animations:^{ self.navigationController.navigationBar.alpha=1; CGFloat top = self.parentViewController.topLayoutGuide.length; CGFloat bottom = self.parentViewController.bottomLayoutGuide.length; if (self.tableView.contentInset.top != top) { UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0); self.tableView.contentInset = newInsets; self.tableView.scrollIndicatorInsets = newInsets; } }]; return YES; } 

It can be reorganized to pull out part of the embedded material, but not allowing it in this exercise.

Starting to publish my "very small idea of ​​what I am doing," as the answer below.

Partial answer

Okay, so I'm trying to output the streams of information into the appropriate signals.

Basically I need to know:

  • I'm currently looking
  • The current value of my contentInset (top) in this case

So my approach will be

  • Create a RACSubject for whether I am currently searching for self.currentlySearchingSignal .
  • Turn the top value of my tableView.contentInset into a signal
  • sendNext:@(YES) to currentlySearchingSignal when searchBarShouldBeginEditing is searchBarShouldBeginEditing (and when it returns YES)
  • sendNext:@(NO) to currentlySearchingSignal when searchBarShouldEndEditing is searchBarShouldEndEditing (and when it returns YES)
  • ...

Ok, I'm stuck. I know that I need to somehow combine / subscribe to them, but try to think about it at a non-state level.

  • When adding to parent VC And when my contentInset.top is not set correctly yet ( topLayoutGuide ), I need to install it without animation.
  • When searching And my contentInset.top set incorrectly (status bar) I need to perform the animation (and then not update it before the animation is completed)
  • When the search is not performed And my contentInset.top set incorrectly ( topLayoutGuide ) I need to perform the animation (and not update it before the animation)

Trying to solve a problem

Here is my beginning. Trying to solve for # 1, but not working yet.

 - (void)viewDidLoad { [super viewDidLoad]; @weakify(self); self.currentlyInSearchMode = [RACSubject subject]; self.contentInsetTop = RACObserve(self.tableView, contentInset); RACSignal *parentViewControllerSignal = RACObserve(self, parentViewController); // setup the insets when added to parent and not correctly set yet [[[RACSignal combineLatest:@[self.contentInsetTop, parentViewControllerSignal]] filter:^BOOL(RACTuple *value) { return !((NSValue *)value.first).UIEdgeInsetsValue.top == ((UIViewController *)value.second).topLayoutGuide.length; }]doNext:^(id x) { @strongify(self); CGFloat top = self.parentViewController.topLayoutGuide.length; CGFloat bottom = self.parentViewController.bottomLayoutGuide.length; if (self.tableView.contentInset.top != top) { UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0); self.tableView.contentInset = newInsets; self.tableView.scrollIndicatorInsets = newInsets; } }]; } 
+7
reactive-cocoa
source share
1 answer

Here is my answer copied from a GitHub issue:

I have not used ReactiveCocoaLayout , but I suspect that you may find that some of this code can be improved using RCL, in addition to RAC. I'm sure someone else will provide more details on this.

The first thing I suggest is to read -rac_signalForSelector: This is extremely valuable for the connection between delegate callbacks and the RAC signal.

For example, here, how you get signals that are your callbacks:

 RACSignal *movedToParentController = [[self rac_signalForSelector:@selector(didMoveToParentViewController:)] filter:^(RACTuple *arguments) { return arguments.first != nil; // Ignores when parent is `nil` }]; RACSignal *beginSearch = [self rac_signalForSelector:@selector(searchBarShouldBeginEditing:)]; RACSignal *endSearch = [self rac_signalForSelector:@selector(searchBarShouldEndEditing:)]; 

Now let's say you have a method that updates the view:

 - (void)updateViewInsets:(UIEdgeInsets)insets navigationBarAlpha:(CGFloat)alpha animated:(BOOL)animated { void (^updates)(void) = ^{ if (self.tableView.contentInset.top != insets.top) { self.tableView.contentInset = insets; self.tableView.scrollIndicatorInsets = insets; } self.navigationController.navigationBar.alpha = alpha; }; animated ? [UIView animateWithDuration:0.25 animations:updates] : updates(); } 

Now you can use start to create several things.

First, since -searchBarShouldBeginEditing: is the shortest:

 [beginSearch subscribeNext:^(id _) { UIEdgeInsets insets = UIEdgeInsetsMake(UIApplication.sharedApplication.statusBarFrame.size.height, 0, 0, 0); [self updateViewInsets:insets navigationBarAlpha:0 animated:YES]; }]; 

Now, for a more complicated piece. This signal composition starts with +merge with two signals, a signal for -didMoveToParentViewController: and a signal for searchBarShouldEndEditing: Each of these signals is mapped to the corresponding parent view controller and is associated with a boolean indicating whether to perform animation. This pair of values ​​is packaged in RACTuple .

Next, using -reduceEach: tuple (UIViewController *, BOOL) mapped to the tuple (UIEdgeInsets, BOOL) . This mapping computes edge inserts from the parent view controller, but does not change the animated flag.

Finally, this signal composition is signed in which the helper method is called.

 [[[RACSignal merge:@[ [movedToParentController reduceEach:^(UIViewController *parent) { return RACTuplePack(parent, @NO); // Unanimated }], [endSearch reduceEach:^(id _) { return RACTuplePack(self.parentViewController, @YES); // Animated }] ]] reduceEach:^(UIViewController *parent, NSNumber *animated) { CGFloat top = parent.topLayoutGuide.length; CGFloat bottom = parent.bottomLayoutGuide.length; UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0); return RACTuplePack(([NSValue valueWithUIEdgeInsets:newInsets]), animated); }] subscribeNext:^(RACTuple *tuple) { RACTupleUnpack(NSValue *insets, NSNumber *animated) = tuple; [self updateViewInsets:insets.UIEdgeInsetsValue navigationBarAlpha:1 animated:animated.boolValue]; }]; 

In RAC you will find alternative approaches that you can use. The more you experiment, the more you discover what works, what doesn't work, nuances, etc.

PS. The corresponding @weakify / @strongify remains as an exercise.

NEXT ANSWER

Another approach is to use -rac_liftSelector: Here's how you can use it for the code you provided. This is very similar to the code above, except that you extract the animated flag into your own signal, instead of inserting it into the signal that calculates the inserts.

 RACSignal *insets = [RACSignal merge:@[ [movedToParentController reduceEach:^(UIViewController *parent) { return parent; }], [endSearch reduceEach:^(id _) { return self.parentViewController; }] ]] map:^(UIViewController *parent) { CGFloat top = parent.topLayoutGuide.length; CGFloat bottom = parent.bottomLayoutGuide.length; UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0); return [NSValue valueWithUIEdgeInsets:newInsets]; }]; RACSignal *animated = [RACSignal merge:@[ [movedToParentController mapReplace:@NO], [endSearch mapReplace:@YES], ]; RACSignal *alpha = [RACSignal return:@1]; [self rac_liftSelector:@selector(updateViewInsets:navigationBarAlpha:animated:) withSignals:insets, alpha, animated, nil]; 

IMO, no approach is a clear winner compared to another. However, recommendations recommend avoiding explicit subscription .

+12
source share

All Articles