首先先抛出问题:能否抓到pop/push完成的回调?就像dismissViewControllerAnimated: completion:一样?
接着我们开始尝试。我们从UINavigationController.h源文件着手,找系统留下的API,看是否能进行扩展。

尝试1

我们看到了pushViewController: animated:popViewControllerAnimated:方法,于是我们猜测是否可以通过重写这两个方法来实现,那我们实践一下,只要想办法重写这两个方法,并在原有方法的基础上加上调用方需要的回调,看回调是否执行就可以得出结论了。这里我采用扩展类的方式来实现。
首先我们新建一个PopedBlock分类,在里面添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *selString = @"popViewControllerAnimated:";
NSString *mySelString = [@"xb_" stringByAppendingString:selString];
Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(selString));
Method myMethod = class_getInstanceMethod(self, NSSelectorFromString(mySelString));
method_exchangeImplementations(originalMethod, myMethod);
});
}

- (void)xb_popViewControllerAnimated:(BOOL)animated {
[self xb_popViewControllerAnimated:animated];
NSLog(@"pop好了");
}

上面代码中,用到了runtime的方法替换。大家可以网上找找,这个东西还是挺好用的。
接着我们引入这个分类,使用以下代码进行跳转。
[self.navigationController popViewController:YES];
进行测试。
测试结果发现:在push的同时控制台打印“pop好了”,这是不行的,因为上述pop是有动画效果的,那么从开始pop到整个页面完全展示是需要时间的;然而在pop的时候就已经执行了打印方法。因此说明在popViewController:这个方法里写回调达不到我们的要求。

尝试2

我们注意到里面有个UINavigationControllerDelegate,它声明了几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@protocol UINavigationControllerDelegate <NSObject>

@optional

// Called when the navigation controller shows a new top view controller via a push, pop or setting of the view controller stack.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;

- (UIInterfaceOrientationMask)navigationControllerSupportedInterfaceOrientations:(UINavigationController *)navigationController NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
- (UIInterfaceOrientation)navigationControllerPreferredInterfaceOrientationForPresentation:(UINavigationController *)navigationController NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;

- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);

@end

里面有一对方法willShowViewController和didShowViewController。从语义可以看出,后者比较符合我们的需求。然后我们看下怎么使用- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated这个方法。
我们注意到UINavigationControllerDelegateUINavigationController的一个delegate。

1
2
3
4
5
6
7
//...
NS_CLASS_AVAILABLE_IOS(2_0) @interface UINavigationController : UIViewController

//...

@property(nullable, nonatomic, weak) id<UINavigationControllerDelegate> delegate;
//...

所以要有一个对象可以来放置UINavigationController的delegate。然而我希望能最大程度的解耦。基于用分类的写法,那么这个对象我们可以选择分类本身。咋看会有问题,但实际不会,因为我们看UINavigationController.h文件中对delegate的声明,用的是weak。所以这个想法是没有问题的。
我们首先先让分类遵守UINavigationControllerDelegate。并实现它的方法:

1
2
3
4
5
6
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
void(^block)(void) = [self block];
if(block){
block();
}
}

我们需要在这个方法里面处理回调,所以我们需要在push之前设置好回调。因此我们给这个分类加个方法,为了使调用简单,我们可以这么写:

1
2
3
4
5
- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated block:(void(^)(void))block {
self.delegate = self;
[self setBlock:block];
return [self popViewControllerAnimated:animated];
}

这样外面只需要一句代码就可以实现这个功能了。多好~
我们在pop的地方替换成

1
2
3
[self.navigationController popViewController:YES block:^(void){
NSLog(@"完全展示");
}];

接着进行测试。
结果:跳转开始时没有任何变化,直到页面完全展示的时候,控制台才打印出“完全展示”。这说明我们确实是拦截到了动画完成的时机。到这里我们似乎实现了想要的目的,但是这时我们再继续返回上一页面,触发pop方法,当页面完全展示时会发现控制台又打印出“完全展示”。
为什么呢?原因很简单,这个delegate是UINavigationController的,并且我们把分类设置了进去,那么在UINavigationController生命周期以内,delegate都是会触发的,而didShowViewController这个方法指的是完全展示之后被触发(不管是pop,还是push,只要是UINavigationController堆栈里面的页面被展示)。所以为了不牵一发而动全身,我们需要“调后即毁”。
这个简单,我们只要在回调执行后把block设置为nil就可以了。那在调用的地方可以这么写:

1
2
3
[self popViewControllerAnimated:YES block:^{
//回调
}];

(假设A push to B,返回的时候,B执行了上述方法),那当B pop to A页面,B页面完全消失后,回调执行。而且其他的pop不会执行回调。

  1. autoresizingMask
    当vc有导航栏且这个vc的tableView设置了tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight;,那么在最终显示的时候tableView的高度会自动减去导航栏的高度。如果你要用代码去控制tableView或它的子view的frame时,要记住这点,避免出现空白
  2. deleteRowsAtIndexPaths和deleteSections
    section有大于1条row时,可以使用deleteRowsAtIndexPaths;当section中只有一条row时,要用deleteSections
  3. 分割线的间距
    系统默认的间距就是15,所以你设置15是跟默认的一样。
    1. xib设置
      xib面板上第4个选项separator Inset设置
    2. 代码设置
      [self.tableView setSeparatorInset:UIEdgeInsetsMake(0, 15, 0, 0)];
  4. 键盘隐藏方式

    1. 拖拽时隐藏

      1
      tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
    2. 触摸时隐藏(手势)

      1
      2
      3
      4
      5
      6
      7
      @weakify(self)
      UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
      [[tap rac_gestureSignal] subscribeNext:^(id x) {
      @strongify(self);
      [self.view endEditing:YES];
      }];
      [self.servicePackageAddView.tableView addGestureRecognizer:tap];
  5. 该使用plain?还是grouped?

    想要撇开讨厌的空白间隙的问题,就要用plain模式。
    实在要用grouped模式,遇到空白间隙时,妥善使用self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);可以帮助你处理好空白问题。

  6. cell自动高度设置
    cell不管是用xib,还是用代码写,只要不是固定大小的都会碰到高度自适应问题。如果不考虑性能,可以将cell高度设置为UITableViewAutomaticDimension。要求cell的子元素必须跟cell做好四条边约束(上、下、左、右),如果遇上iOS10及以下版本,还需要设置
    self.tableView.estimatedRowHeight = 某个值;不然没有效果。