題外話:最近一直很閑,項目上基本沒有啥大的需求。對于程序員來說,如果沒有需求其實是一件很難受的事情,之前好多次在項目中沒事找事,該優化的優化,該整理的整理??赡芎枚喑绦騿T都遇到過與我類似的情況。但是程序員真的需要通過刷項目去提高自己嗎?程序員的功力體現在處理項目的細節上,有可能為了改進一點點的體驗就要付出很大的代價,就要接觸更多的知識點。做的項目多,但是不精那么只能代表自己在比較淺顯的領域比較熟練而已。以上觀點是我在最近思考得來的,并不一定正確,甚至明天一早我就會推翻這種想法,請大家謹慎參考??。
一、什么是隔離導航控制器?
這個名詞純粹是我自己瞎編的,我不知道有沒有這種說法,至少我沒有看到。我所說的隔離導航控制器是指:在導航欄樣式不同的頁面采用不同的導航控制器?,F在同一個導航欄發生變化主要體現在:隱藏導航欄頁面跳轉到非隱藏導航欄的頁面、A顏色導航欄的頁面跳轉到B顏色導航欄的頁面、可跟隨滑動做動畫的頁面跳轉到固定導航欄的頁面。隔離導航控制器就是:當頁面跳轉到導航欄不同的頁面時不再使用同一個導航控制器,而是彈出一個新的導航控制器,這個導航控制器的導航欄固定不變,如果發生變化那么再彈出一個新的導航控制器。
二、為什么要隔離導航控制器?
很簡單,因為我已經受夠了頁面的來回跳轉導致狀態欄儲存、變化、恢復這樣的過程。尤其在側滑或者全屏滑動返回時,導航欄的變化會有不好的體驗。所以在很久以前我就在醞釀,如果present出來一個導航控制器就好了(隔離導航控制器并不是一個很好地解決辦法,但是可以試一試,畢竟我很閑)。因此我在項目中試了一下,結果遇到了一些問題,自己挖的坑,跪著也要填滿了。
三、實現右進右出的present
ViewController 的present樣式一共有4種:
typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
UIModalTransitionStyleCoverVertical = 0,//默認
UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,//翻轉
UIModalTransitionStyleCrossDissolve,//透明度漸變
UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,//翻書
};
[vc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal]; //present 前加上這句就行
[self presentViewController:vc animated:YES completion:nil];
沒有我們需要的效果,因此我們需要利用iOS 7 以后的轉場動畫來實現。說到轉場動畫不怕大家笑話,我已經看了好多遍了,但是依然記不住那些名字。每次要用都要去大神的博客里再學習一邊。我的代碼也是使用大神的代碼,因為覺得沒有必要再寫一遍。只不過我做了略微的修改。
// NormalDismissAnimation
// 4. Do animate now
NSTimeInterval duration = [self transitionDuration:transitionContext];
toVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是為了上下兩個控制器有聯動的效果
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
fromVC.view.frame = finalFrame;
toVC.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
//BouncePresentAnimation
// 4. Do animate now
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
toVC.view.frame = finalFrame;
fromVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是為了有上下兩個控制器聯動的效果
} completion:^(BOOL finished) {
fromVC.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:YES];
}];
//SwipeUpInteractiveTransition
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
// 1. Mark the interacting flag. Used when supplying it in delegate.
self.interacting = YES;
[self.presentingVC dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged: {
// 2. Calculate the percentage of guesture
CGSize screenSize = [UIScreen mainScreen].bounds.size;
CGFloat fraction = translation.x / screenSize.width;
//Limit it between 0 and 1
fraction = fminf(fmaxf(fraction, 0.0), 1.0);
self.shouldComplete = (fraction > 0.4);
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
// 3. Gesture over. Check if the transition should happen or not
self.interacting = NO;
if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];
} else {
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
以上部分很簡單而且并不是我們所關心的部分(雖然名字有點難記憶,但是不難理解)。完成以上代碼,就可以實現滑動返回+右進右出的present樣式了。
//還有一點需要注意的是,需要禁止全屏手勢在導航控制器棧里有多個元素時響應
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if ([self.presentingVC isKindOfClass:[BaseNavigationController class]]) {
BaseNavigationController *nav = (BaseNavigationController*)self.presentingVC;
if (nav.viewControllers.count >=2) {
return NO;
}
}
return YES;
}
四、遇到的問題
我們(或者說大多數APP)都有這樣的需求,當收到推送時需要從底部彈出一個ViewController。 但是因為我們是present 出來了一些頁面,當收到推送的時候我們并不能一下子知道從哪個ViewController 上present 出來一個需要用戶響應的控制器。比方說 HomeViewController ---(右進右出present)---->ReportMessageViewController后,從代碼的角度我們不能一下子拿到展現在用戶面前的控制器(因為你不知道是否已經present了,也不知道誰調用的present)。因此我遇到了第一個問題(很多的項目可以通過tab、nav 來確定,而且present 出控制器的場景比較固定):
1、到底誰是當前正在響應的控制器?
要想解決這個問題我們首先要知道響應者鏈條。即當我們點擊了屏幕上的一個按鈕,事件是怎么傳遞的。UIView 和 UIViewController 都是繼承自UIResponder的,他們都可以成為響應者。因此我們只要遍歷屏幕上最上方的那些View(葉子節點),縱向循環找到他們的nextResponder,直至找到為UIViewController類的響應者。
// 獲取某個view 的葉子 View(一般為Window)
/*
*/
+(NSMutableArray *)getTopSubViewsWithParentView:(UIView *)rootView{
NSMutableArray *stack = [NSMutableArray array];
NSMutableArray *leafNodes = [NSMutableArray array];//存放葉子節點
if (rootView.subviews.count == 0) {
return nil;
}
[stack addObjectsFromArray:rootView.subviews];//把根視圖的所有第一層子視圖入棧
while (stack.count != 0) {
UIView *subView = [stack lastObject];//取出頂部元素并判斷是否為葉子節點
[stack removeLastObject];
if (subView.subviews.count != 0) {//不是葉子節點的話將其子視圖繼續入棧
[stack addObjectsFromArray:subView.subviews];
}else{
[leafNodes addObject:subView];//如果是葉子節點則將其入棧(葉子節點的棧)
}
}
return leafNodes;
}
//獲取某個視圖在哪個控制器上
+(UIViewController*)getViewControllerWithView:(UIView*)view{
UIResponder *res = view;
while (res) {
if (res.nextResponder) {
res = res.nextResponder;
}
if ([res isKindOfClass:[UIViewController class]]) {
UIViewController *vc = (UIViewController*)res;
return vc;
}
}
return nil;
}
//這段代碼的意思是,如果我能判斷的更精確就精確些。比如某個導航控制器,你說他在響應也行,他的top元素在響應也行,顯然我想精確到top元素
+(UIViewController*)getCurrentVC{
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
NSMutableArray *array = [self getTopSubViewsWithParentView:keyWindow];
UINavigationController *nav = nil;
UITabBarController *tab = nil;
for (UIView *subView in array) {
UIViewController *vc = [self getViewControllerWithView:subView];
if (!([vc isKindOfClass:[UINavigationController class]] || [vc isKindOfClass:[UITabBarController class]])) {
return vc;
}
if ([vc isKindOfClass:[UINavigationController class]]) {
nav = (UINavigationController*)vc;
}
if ([vc isKindOfClass:[UITabBarController class]]) {
tab = (UITabBarController *)vc;
}
}
if (nav) {
return nav;
}
if (tab) {
return tab;
}
return nil;
}
有了這些代碼,問題一就解決了。
2、全屏滑動遇到了可以左右滑動的ScrollView怎么辦?
首先說明一下,這個問題我解決的并不完美,因為去除了bounces 效果。因為滑動ScrollView時,ScrollView 的pan 手勢會優先響應,并阻止其他手勢響應。首先我們先看一下手勢代理,看看都有哪些方法:
1 @protocol UIGestureRecognizerDelegate <NSObject>
2 @optional
3 // called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
4 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
5
6 // called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
7 // return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
8 //
9 // note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
10 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;//手勢1、手勢2 是否可以共存,即兩者都響應,收到事件繼續傳遞下去
11
12 // called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
13 // return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
14 //
15 // note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
16 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);//在other 響應的情況下,自己是否不響應
17 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
18
19 // called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
20 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
21
22 // called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press
23 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;
通過打印scrollView 的pan手勢我們會發現 pan手勢的代理是scrollView ,我嘗試過改變pan 的delegate 但是發現會崩潰,apple 是不允許改變這個delegate 的。所以我想到了寫一個UIScrollView的子類。
//
// PanScrollView.m
// Property
//
// Created by 高雅馨on 16/7/25.
// Copyright ? 2016年 高雅??. All rights reserved.
//
#import "PanScrollView.h"
@implementation PanScrollView
-(instancetype)init{
if (self = [super init]) {
self.bounces = NO;
}
return self;
}
/*
是否將相應傳遞給other
當偏移量X值為0的時候全屏手勢和pan 手勢同時響應。全屏手勢向右滑動時 ,由于bounces效果已經被去掉了,所以偏移量不變,ViewController 只有dismiss 效果。當向左滑動時全屏手勢沒有任何效果,只要scroll效果。
*/
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if (gestureRecognizer == self.panGestureRecognizer && otherGestureRecognizer == self.otherGes){
if (self.contentOffset.x==0) {
return YES;
}
}
return NO;
}
@end
至此,大部分可見的問題解決了,可能并不完美,也不適合,但是嘗試也是一種美好的回憶,希望能對大家有幫助。