iOS:轻量可定制的防键盘遮挡textField实现总结

背景

  这是个常见场景:textField或者包含textField的控件需要在键盘弹出的时候随之上移,不然就会被键盘遮挡。

  既然是常见的,为了提高开发效率,也为了遵循DRY原则,我们就有必要实现一个公共控件。实现这个功能并不复杂,更有意义的是在这个实现过程中的一些总结和思考。下面首先讲一下实现过程,之后再附上总结。

实现

  在键盘弹出和收起的时候,会收到两个全局的系统通知:UIKeyboardWillShowNotification和UIKeyboardWillHideNotification,并且通知的userInfo中包含有键盘高度和键盘展开及收起的动画时间。键盘高度可以推算出上移的高度,而上移下移动画时间与键盘展开收起动画时间保持一致可以使得动画更加流畅。

  一般来说,需要上移的高度就是textField底部和键盘顶部的距离,不过也有一些场景需要上移更多的距离,比如,textField下方还有个确认按钮,那这种情况可能需要把确认按钮也移到键盘的上方,此时一共需要上移的高度,就应该是键盘顶部与textField底部之间的距离,加上textField底部与确认按钮底部的距离。

  一般情况下直接上移整个keyWindow即可,不过也有一些场景是需要移动一个特定的view,比如承载textField的容器。

  考虑到以上因素,我们来做一个比较灵活的可定制的防止键盘遮挡textField,通过UITextField子类来实现。代码如下:

#import 

@interface LHWAutoAdjustKeyboardTextField : UITextField

//上移后,textField需要额外高于键盘顶部的距离,默认为0
@property (nonatomic, assign) CGFloat offset;

//需要向上移动的view,默认为keyWindow
@property (nonatomic, weak) UIView *movingView;

@end
#import "LHWAutoAdjustKeyboardTextField.h"

@interface LHWAutoAdjustKeyboardTextField()

@end

@implementation LHWAutoAdjustKeyboardTextField

#import "LHWAutoAdjustKeyboardTextField.h"

@interface LHWAutoAdjustKeyboardTextField()

@property (nonatomic, assign) CGRect originalFrame;

@end

@implementation LHWAutoAdjustKeyboardTextField

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self addKeyboardNotifications];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self addKeyboardNotifications];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

- (void)addKeyboardNotifications {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}

- (void)keyboardWillShow: (NSNotification *)notification {
    if (self.isFirstResponder) {
        CGPoint relativePoint = [self convertPoint: CGPointZero toView: [UIApplication sharedApplication].keyWindow];

        CGFloat keyboardHeight = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
        CGFloat overstep = CGRectGetHeight(self.frame) + relativePoint.y + keyboardHeight - CGRectGetHeight([UIScreen mainScreen].bounds);
        overstep += self.offset;
        self.originalFrame = self.movingView.frame;
        if (overstep > 0) {
            CGFloat duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
            CGRect frame = self.originalFrame;
            frame.origin.y -= overstep;
            [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
                self.movingView.frame = frame;
            } completion: nil];
        }
    }
}

- (void)keyboardWillHide: (NSNotification *)notification {
    if (self.isFirstResponder) {
        CGFloat duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
        [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
            self.movingView.frame = self.originalFrame;
        } completion: nil];
    }
}

@end

总结

  总结下实现过程中值得注意的几个细节:

  (1)为什么选择用继承而不是用分类?首先我们要明白分类的主要目标在于扩展功能,而非数据。本例中除了需要拓展UITextField的功能,还需要保存额外的数据(offset,movingView以及originalFrame),因此更适合用继承。

  有同学可能会有疑问了,用runtime的关联对象可以为分类添加属性啊扩展数据啊,嗯,确实可以,但是关联对象不到不得已或者是调试场景下,尽量不要使用,因为很容易引发奇怪的内存管理问题。

  (2)在dealloc函数里要移除对键盘事件的通知,不然在iOS8系统会crash,这也是不考虑用分类实现的另一个原因,在分类中override已有方法是非常危险的,尤其是dealloc这种控制生命周期的函数

  分类的方法加入原有类这一操作是在运行期系统加载分类时完成的,所以很可能会覆盖原有类的实现,如果有多个分类同时实现了名字一样的方法,结果就是以最后一次的覆盖为准。因此,在实现分类方法时,仅仅避免覆写已有方法还不够,最好还要加上前缀,来避免工程中其他地方的某个分类和你的分类起了一样的名字,不然出现bug后会很难定位;

  (3)本自定义类的前缀是LHW,三个字母开头,因为苹果宣称保留所有两个字母前缀的权利,所以AFNetworking、SDWebImage等等这些著名开源库严格来说命名是不符合苹果规范的;

  (4)上移的view,这个属性要定义成weak的,因为很可能这个view就是textField的superView,如果不声明成weak,将会导致循环引用。

  很多人对weak的理解仅仅局限在防止循环引用的层面上,其实weak有更深层次的含义。在本例中,即便不会引发循环引用,上移的view也更适合于声明成weak的,因为这个类对于上移view是仅仅知道就可以的弱关联关系,而不是一种拥有或者持有的强关联关系。考虑另外一个相似的场景:在可以方便使用block回调的UIAlertController出现以前,当一个VC实现多个alertView的代理回调时,我们常常通过属性保存这些alertView来区分(用tag区分是很不优雅的做法)。

#import "FooVC.h"

@interface FooVC() <UIAlertViewDelegate>

@property(nonatomic, weak) UIAlertView *alertViewA;
@property(nonatomic, weak) UIAlertView *alertViewB;
@property(nonatomic, weak) UIAlertView *alertViewC;

@end

@implementation FooVC

- (void)showAlertABC {
    UIAlertView *alertViewA = [[UIAlertView alloc] initWithTitle:@"" message:@"我是弹窗A" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"doAThing", nil];
    self.alertViewA = alertViewA;
    UIAlertView *alertViewB = [[UIAlertView alloc] initWithTitle:@"" message:@"我是弹窗B" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"doBThing", nil];
    self.alertViewB = alertViewB;
    UIAlertView *alertViewC = [[UIAlertView alloc] initWithTitle:@"" message:@"我是弹窗C" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"doCThing", nil];
    self.alertViewC = alertViewC;
    [alertViewA show];
    [alertViewB show];
    [alertViewC show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (alertView == self.alertViewA) {
        [self doAThing];
    } else if (alertView == self.alertViewB) {
        [self doBThing];
    } else  if (alertView == self.alertViewC) {
        [self doCThing];
    }
}

@end

  此时就应该声明成weak而非strong,声明成weak的好处是VC不会干扰这些alertView原本的生命周期,如果声明成strong的,相当于强行延长了这些alertView的声明周期,直到VC释放时,他们才能释放,这样做显然是不合理的。

  (5)设计公用控件时,要尽可能多考虑各种使用场景,抽象出可定制部分,如本例中的offset和movingView,如果一开始没考虑这些,把原本需要定制的元素在代码中写死,等到未来需要时,就不得不改动原有的实现,违背了设计模式的开闭原则,非常不好。

作者:Eternal_Love

猜你喜欢

转载自blog.csdn.net/ios8988/article/details/82813096