Just trying to wrap my head around the ReactiveCocoa approach to certain situations.
I have a situation where a segment controller swaps out children view controllers. I need to accomplish a couple things here:
contentInset
of the tableView
because iOS7 doesn't handle it for me with custom container viewscontentInset
with animationnavigationBar
back in and reset the contentInset
in an animationHere is the current code that accomplishes 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 could be refactored to pull some of the inset stuff out, but keeping it flat for this exercise.
Going to post my 'very little idea what I'm doing' approach as an answer below.
Ok so I'm trying to pull out streams of information into relevant signals.
Basically I need to know:
contentInset
(top) in this caseSo my approach would be
self.currentlySearchingSignal
.top
value of my tableView.contentInset
into a signalsendNext:@(YES)
to currentlySearchingSignal
when searchBarShouldBeginEditing
is called (and when it will return YES)sendNext:@(NO)
to currentlySearchingSignal
when searchBarShouldEndEditing
is called (and when it will return YES)Ok I'm stuck. I know I need to combine/subscribe to these somehow, but trying to think of it in a non-state way.
contentInset.top
isn't yet set properly (topLayoutGuide
), I need to set it without an animation.contentInset.top
isn't set properly (status bar frame) I need to perform the animation (and then not update this again until my animation is done)contentInset.top
isn't set properly (topLayoutGuide
) I need to perform the animation (and not update again until the animation is done)Here's my start. Trying to solve for #1, but it's 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;
}
}];
}
Here's my answer, as copied from the GitHub issue:
I haven't used ReactiveCocoaLayout, but I suspect you may find some of this code could be improved by using RCL, in addition to RAC. I'm sure someone else will provide more detail about that.
The first thing I'd suggest is reading up on -rac_signalForSelector:
. It's enormously valuable for bridging between delegate callbacks and RAC signal.
For example, here's how you get signals that represent your desired 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 put a few things together.
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 the more complicated piece. This signal composition starts by +merge
ing two signals, the signal for -didMoveToParentViewController:
and the signal for searchBarShouldEndEditing:
. Each of these signals is mapped to the appropriate parent view controller, and paired with a boolean indicating whether to perform animation. This pair of values is packed into a RACTuple
.
Next, using -reduceEach:
, the (UIViewController *, BOOL)
tuple is mapped into a (UIEdgeInsets, BOOL)
tuple. This mapping calculates the edge insets from the parent view controller, but doesn't change animated
flag.
Finally, this signal composition is subscribed to, wherein 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];
}];
You'll find with RAC there are often alternative approaches that you can take. The more you experiment, the more you'll discover what works, what doesn't work, the nuances, etc.
PS. Appropriate @weakify
/@strongify
is left as an exercise.
FOLLOW UP ANSWER
Another approach is to use -rac_liftSelector:
. Here's how it could be used for the code you've provided. It's very similar to the code above, except you extract the animated
flag into its own signal, instead of nesting it into the signal that calculates the insets.
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, neither approach is a clear winner over the other. The guidelines however do recommend avoiding explicit subscription.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With