前言
修改项目中的聊天页面,键盘仿照微信的键盘布局效果,于是乎在码云上搜索到了这个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];
}
复制代码
然后,执行,报错。。。
这个问题一直没有解决,初始化添加个方法 -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;
}
复制代码