【iOS】UITextView 添加 placeholder

·  阅读 559

前言

修改项目中的聊天页面,键盘仿照微信的键盘布局效果,于是乎在码云上搜索到了这个demo。其中作者关于 UITextView 添加 placeholder 使用的继承,我不想使用继承,那么就使用类别实现。

1、给类别添加两个属性 placeHolder 和 placeHolderTextColor ,添加占位文字和占位文字颜色;

2、类别中添加属性要手动实现 getter 和 setter 方法。

- (NSString *)placeHolder
{
    return objc_getAssociatedObject(self, @selector(placeHolder));
}

- (void)setPlaceHolder:(NSString *)placeHolder
{
    NSString *_placeHolder = objc_getAssociatedObject(self, @selector(placeHolder));
    if ([_placeHolder isEqualToString:placeHolder]) {
        return;
    }
    objc_setAssociatedObject(self, @selector(placeHolder), placeHolder, OBJC_ASSOCIATION_COPY);
}

- (UIColor *)placeHolderTextColor
{
    UIColor *_placeHolderTextColor = objc_getAssociatedObject(self, @selector(placeHolderTextColor));
    if (_placeHolderTextColor == nil) {
        _placeHolderTextColor = [UIColor lightGrayColor];
    }
    return _placeHolderTextColor;
}

- (void)setPlaceHolderTextColor:(UIColor *)placeHolderTextColor
{
    NSString *_placeHolderTextColor = objc_getAssociatedObject(self, @selector(placeHolderTextColor));
    if ([_placeHolderTextColor isEqual:placeHolderTextColor]) {
        return;
    }
    objc_setAssociatedObject(self, @selector(placeHolderTextColor), placeHolderTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
复制代码

1、遇到的问题

1.1、重写初始化方法 -init

我要在初始化的时候设置 placeholderTextColor 和 font 的初始值,这个值也是 placeholder 的字体属性,同时要监听文本变化的通知来重新绘制界面文字,有内容不显示 placeholder,text 为空则显示。

开始不打算使用在 +load 方法交换方法的方式实现,直接重写,然后再调一下主类的方法。因为在load中实现的话,项目启动就会执行,我只想在用到的地方再调用相关代码。

首先重写后会有警告 Convenience initialzer missing a self call to another initializer。

忽略掉初始化警告

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)init
#pragma clang diagnostic pop
复制代码

因为类别里实现了主类的方法的话,只会执行类别里的方法,不会执行主类的方法,所以使用 SEL 和 IMP 来获取主类的方法执行。


#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
/// 重写初始化类,初始化时执行添加通知,监听文本变化
- (instancetype)init
#pragma clang diagnostic pop
{
    // 获取主类初始化方法
    Method method = [self methodOfSelector:@selector(init)];
    SEL sel = method_getName(method);
    IMP imp = method_getImplementation(method);
    self = ((id (*)(id, SEL))imp)(self,sel);
    if (self) {
        self.font = [UIFont systemFontOfSize:16];
        self.placeholder = nil;
        self.placeholderTextColor = [UIColor lightGrayColor];
        [self _addTextViewNotificationObservers];
    }
    return self;
}

- (Method)methodOfSelector:(SEL)selector
{
    u_int count;
    Method *methods = class_copyMethodList([self class], &count);
    NSInteger index = 0;
    
    for (int i = 0; i < count; i++) {
        SEL name = method_getName(methods[i]);
        NSString *strName = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];

        if ([strName isEqualToString:NSStringFromSelector(selector)]) {
            index = i;  // 获取原类方法在方法列表中的索引
        }
    }
    return methods[index];
}


复制代码

然后,执行,报错。。。

WX20210526-142659@2x.png

这个问题一直没有解决,初始化添加个方法 -initWithPlaceholder:。

初始化问题解决了,但是额外添加了初始化方法,有点小不爽,先这样吧,看看效果。

1.2、重写 -setText: 方法

在类别里重写了这个方法,为了设置 text 值的时候执行 [self setNeedsDisplay],在 -drawRect: 里判断是否显示占位文字。

- (void)setText:(NSString *)text
{
    [self _performOverridePrimarySelector:@selector(setText:) withParam:text];
    [self setNeedsDisplay];
}

- (void)_performOverridePrimarySelector:(SEL)selector withParam:(id)param
{
    Method method = [self methodOfSelector:selector];
    SEL sel = method_getName(method);
    IMP imp = method_getImplementation(method);
    ((void (*)(id, SEL, ...))imp)(self,sel, param);
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    if ([self.text length] == 0 && self.placeHolder) {
        [self.placeHolderTextColor set];
        [self.placeHolder drawInRect:CGRectInset(rect, 7.0f, 7.5f) withAttributes:[self _placeHolderTextAttributes]];
    }
}
复制代码

然而,没效果,方法没有进来,文字在占位文字上方重叠在一起。

放弃,使用 Method Swizzling

2、交换方法实现在类别中调用主类的方法

在类别中调用主类方法,实现数据初始设定和内容更新时更新占位文字是否显示。

使用 Method Swizzling 时,一定要在 +load 方法中执行,避免多次执行。

2.1、使用 runtime 交换方法

/// 交换方法
/**
 * 使用 runtime 交换方法
 */
+ (BOOL)hookOrigInstanceMethod:(SEL)oriSEL newInstanceMethod:(SEL)swizzledSEL
{
    Class cls = self;
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!swiMethod) {
        return NO;
    }
    
    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
    return YES;
}
复制代码

2.2、在 +load 中交换方法

交换 -init,-setText: 等

+ (void)load
{
    [self hookOrigInstanceMethod:@selector(init) newInstanceMethod:@selector(yl_init)];
    [self hookOrigInstanceMethod:@selector(setText:) newInstanceMethod:@selector(yl_setText:)];
    [self hookOrigInstanceMethod:@selector(setAttributedText:) newInstanceMethod:@selector(yl_setAttributedText:)];
    [self hookOrigInstanceMethod:@selector(setFont:) newInstanceMethod:@selector(yl_setFont:)];
    [self hookOrigInstanceMethod:@selector(setTextAlignment:) newInstanceMethod:@selector(yl_setTextAlignment:)];
}

- (instancetype)yl_init
{
    // 这里使用 self 报错,所以定义了一个变量ylSelf
    id ylSelf = [self yl_init];
    if (ylSelf) {
        self.font = [UIFont systemFontOfSize:16];
        self.placeHolderTextColor = [UIColor lightGrayColor];
        self.placeHolder = nil;
        [self _addTextViewNotificationObservers];
    }
    return ylSelf;
}

- (void)yl_setText:(NSString *)text
{
    [self yl_setText:text];
    [self setNeedsDisplay];
}

- (void)yl_setAttributedText:(NSAttributedString *)attributedText
{
    [self yl_setAttributedText:attributedText];
    [self setNeedsDisplay];
}

- (void)yl_setFont:(UIFont *)font
{
    [self yl_setFont:font];
    [self setNeedsDisplay];
}

- (void)yl_setTextAlignment:(NSTextAlignment)textAlignment
{
    [self yl_setTextAlignment:textAlignment];
    [self setNeedsDisplay];
}
复制代码

注意是 [self setNeedsDisplay] 不要写成 [self setNeedsLayout].

3、监听文本变化更新布局

文本内容变化时执行 [self setNeedsDisplay],调用 -drawRect: 方法更新布局,判断是否显示占位文字。

初始化(init)时添加监听,释放(dealloc)时移除监听。

- (void)_addTextViewNotificationObservers
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didReceiveTextViewNotification:)
                                                 name:UITextViewTextDidChangeNotification
                                               object:self];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didReceiveTextViewNotification:)
                                                 name:UITextViewTextDidBeginEditingNotification
                                               object:self];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didReceiveTextViewNotification:)
                                                 name:UITextViewTextDidEndEditingNotification
                                               object:self];
}

- (void)_didReceiveTextViewNotification:(NSNotification *)notification
{
    [self setNeedsDisplay];
}

- (void)_removeTextViewNotificationObservers
{
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UITextViewTextDidChangeNotification
                                                  object:self];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UITextViewTextDidBeginEditingNotification
                                                  object:self];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UITextViewTextDidEndEditingNotification
                                                  object:self];

}
复制代码

4、使用

最后就可以直接用了。

self.textView = [[UITextView alloc] init];
self.textView.placeHolder = @"say something";
self.textView.placeHolderTextColor = [UIColor lightGrayColor];
复制代码

使用类别的好处就是对原类侵入性小,只是额外添加了两个属性,其他该怎么写怎么写。(所以这就是我不想学rac和swift的理由吗,重新学一套东西,好累。。。懒癌晚期,没治了。)

5、源码贴上

5.1、UITextView+YLPlaceHolder.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UITextView (YLPlaceHolder)

/// 占位文字颜色
@property (nonatomic, strong) UIColor *placeHolderTextColor;
/// 占位文字
@property (nonatomic, copy  ) NSString * _Nullable placeHolder;

@end

NS_ASSUME_NONNULL_END
复制代码

5.2、UITextView+YLPlaceHolder.m

#import "UITextView+YLPlaceHolder.h"
#import <objc/runtime.h>
#import "NSObject+MethodSwizzling.m"

@implementation UITextView (YLPlaceHolder)

#pragma mark - Property

+ (void)load
{
    [self hookOrigInstanceMethod:@selector(init) newInstanceMethod:@selector(yl_init)];
    [self hookOrigInstanceMethod:@selector(setText:) newInstanceMethod:@selector(yl_setText:)];
    [self hookOrigInstanceMethod:@selector(setAttributedText:) newInstanceMethod:@selector(yl_setAttributedText:)];
    [self hookOrigInstanceMethod:@selector(setFont:) newInstanceMethod:@selector(yl_setFont:)];
    [self hookOrigInstanceMethod:@selector(setTextAlignment:) newInstanceMethod:@selector(yl_setTextAlignment:)];
}

- (instancetype)yl_init
{
    // self 报错 Cannot assign to 'self' outside of a method in the init family
    id ylSelf = [self yl_init];
    if (ylSelf) {
        self.font = [UIFont systemFontOfSize:16];
        self.placeHolderTextColor = [UIColor lightGrayColor];
        self.placeHolder = nil;
        [self _addTextViewNotificationObservers];
    }
    return ylSelf;
}

- (void)yl_setText:(NSString *)text
{
    [self yl_setText:text];
    [self setNeedsDisplay];
}

- (void)yl_setAttributedText:(NSAttributedString *)attributedText
{
    [self yl_setAttributedText:attributedText];
    [self setNeedsDisplay];
}

- (void)yl_setFont:(UIFont *)font
{
    [self yl_setFont:font];
    [self setNeedsDisplay];
}

- (void)yl_setTextAlignment:(NSTextAlignment)textAlignment
{
    [self yl_setTextAlignment:textAlignment];
    [self setNeedsDisplay];
}

- (NSString *)placeHolder
{
    return objc_getAssociatedObject(self, @selector(placeHolder));
}

- (void)setPlaceHolder:(NSString *)placeHolder
{
    NSString *_placeHolder = objc_getAssociatedObject(self, @selector(placeHolder));
    if ([_placeHolder isEqualToString:placeHolder]) {
        return;
    }
    objc_setAssociatedObject(self, @selector(placeHolder), placeHolder, OBJC_ASSOCIATION_COPY);
}

- (UIColor *)placeHolderTextColor
{
    UIColor *_placeHolderTextColor = objc_getAssociatedObject(self, @selector(placeHolderTextColor));
    if (_placeHolderTextColor == nil) {
        _placeHolderTextColor = [UIColor lightGrayColor];
    }
    return _placeHolderTextColor;
}

- (void)setPlaceHolderTextColor:(UIColor *)placeHolderTextColor
{
    NSString *_placeHolderTextColor = objc_getAssociatedObject(self, @selector(placeHolderTextColor));
    if ([_placeHolderTextColor isEqual:placeHolderTextColor]) {
        return;
    }
    objc_setAssociatedObject(self, @selector(placeHolderTextColor), placeHolderTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - Draw

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    if ([self.text length] == 0 && self.placeHolder) {
        [self.placeHolderTextColor set];
        [self.placeHolder drawInRect:CGRectInset(rect, 7.0f, 7.5f) withAttributes:[self _placeHolderTextAttributes]];
    }
}

#pragma mark - Override

- (void)_performOverridePrimarySelector:(SEL)selector withParam:(id)param
{
    Method method = [self methodOfSelector:selector];
    SEL sel = method_getName(method);
    IMP imp = method_getImplementation(method);
    ((void (*)(id, SEL, ...))imp)(self,sel, param);
}

- (Method)methodOfSelector:(SEL)selector
{
    u_int count;
    Method *methods = class_copyMethodList([self class], &count);
    NSInteger index = 0;
    
    for (int i = 0; i < count; i++) {
        SEL name = method_getName(methods[i]);
        NSString *strName = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];

        if ([strName isEqualToString:NSStringFromSelector(selector)]) {
            index = i;  // 先获取原类方法在方法列表中的索引
        }
    }
    return methods[index];
}

#pragma mark - Notifications
- (void)_addTextViewNotificationObservers
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didReceiveTextViewNotification:)
                                                 name:UITextViewTextDidChangeNotification
                                               object:self];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didReceiveTextViewNotification:)
                                                 name:UITextViewTextDidBeginEditingNotification
                                               object:self];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didReceiveTextViewNotification:)
                                                 name:UITextViewTextDidEndEditingNotification
                                               object:self];
}

- (void)_didReceiveTextViewNotification:(NSNotification *)notification
{
    [self setNeedsDisplay];
}

- (void)_removeTextViewNotificationObservers
{
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UITextViewTextDidChangeNotification
                                                  object:self];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UITextViewTextDidBeginEditingNotification
                                                  object:self];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UITextViewTextDidEndEditingNotification
                                                  object:self];

}

- (NSDictionary *)_placeHolderTextAttributes
{
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
    paragraphStyle.alignment = self.textAlignment;
    
    return @{
        NSFontAttributeName: self.font,
        NSForegroundColorAttributeName: self.placeHolderTextColor,
        NSParagraphStyleAttributeName: paragraphStyle
    };
}

- (void)dealloc
{
    [self _removeTextViewNotificationObservers];
}
复制代码

5.3、NSObject+MethodSwizzling.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (MethodSwizzling)

// 交换方法
+ (BOOL)hookOrigInstanceMethod:(SEL)oriSEL newInstanceMethod:(SEL)swizzledSEL;

@end

NS_ASSUME_NONNULL_END
复制代码

5.4、NSObject+MethodSwizzling.m

#import "NSObject+MethodSwizzling.h"
#import <objc/message.h>

@implementation NSObject (MethodSwizzling)

/// 交换方法
/**
 * 使用 runtime 交换方法
 */
+ (BOOL)hookOrigInstanceMethod:(SEL)oriSEL newInstanceMethod:(SEL)swizzledSEL
{
    Class cls = self;
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!swiMethod) {
        return NO;
    }
    
    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
    return YES;
}
复制代码
分类:
iOS
标签: