问题
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:
- When moved to the parent controller, I must update the
contentInset
of thetableView
because iOS7 doesn't handle it for me with custom container views - When search is initiated, I need to fade the navigation bar, and update the
contentInset
with animation - When search ends, I need to fade the
navigationBar
back in and reset thecontentInset
in an animation
Here 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.
Partial Answer
Ok so I'm trying to pull out streams of information into relevant signals.
Basically I need to know:
- Am I currently searching
- The current value of my
contentInset
(top) in this case
So my approach would be
- Create a RACSubject for whether or not I am currently searching
self.currentlySearchingSignal
. - Turn the
top
value of mytableView.contentInset
into a signal sendNext:@(YES)
tocurrentlySearchingSignal
whensearchBarShouldBeginEditing
is called (and when it will return YES)sendNext:@(NO)
tocurrentlySearchingSignal
whensearchBarShouldEndEditing
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.
- When added to the parent VC AND when my
contentInset.top
isn't yet set properly (topLayoutGuide
), I need to set it without an animation. - When searching AND my
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) - When not searching AND my
contentInset.top
isn't set properly (topLayoutGuide
) I need to perform the animation (and not update again until the animation is done)
Trying to solve it
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;
}
}];
}
回答1:
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.
来源:https://stackoverflow.com/questions/19170122/approach-to-reactifying-delegate-methods-with-side-effects