preface
I modified the chat page in the project, and the keyboard imitated the keyboard layout effect of wechat, so I found this demo in the code cloud. I don’t want to use inheritance, so I’ll use class implementation.
PlaceHolder text color placeHolder text color placeHolder text color placeHolder text color placeHolder text color
To add a property to a category, manually implement getter and setter methods.
- (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);
}
Copy the code
1. Problems encountered
1.1, override initialization method -init
PlaceholderTextColor/font properties while the placeholdertext is placeholderplaceholdertext properties while listen for notifications of text changes to redraw the placeholder text. If text is empty, it is displayed.
Instead of using the +load method to swap methods, we’ll override them and then tune the main class’s methods. Because when implemented in Load, the project starts and executes, I just want to call the code where it’s needed.
The initial override will warn the Convenience Initialzer missing a self call to another Initializer.
Ignore the initialization warning
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)init
#pragma clang diagnostic pop
Copy the code
Because the category to implement the method of the main class, will only execute the method of the category, will not execute the method of the main class, so use SEL and IMP to get the method of the main class execution.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
/// overwrite the initializer class, perform add notification during initialization, listen for text changes
- (instancetype)init
#pragma clang diagnostic pop
{
// Get the primary class initialization method
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; // Get the index of the original method in the method list}}return methods[index];
}
Copy the code
Then, execute, report an error…
This problem has not been solved. Initialize with placeholder:.
Initialization problem solved, but added an additional initialization method, a little bit uncomfortable, let’s do this for now, see the effect.
1.2, override -settext: method
Override this method in the category to execute [self setNeedsDisplay] to set the text value, and determine whether to display placeholder text in -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]]; }}Copy the code
However, there is no effect, the method does not come in, and the text overlaps on top of the placeholder text.
Give up and use Method Swizzling.
2. Swap methods implement methods that call the main class in a class
Call the main class method in the category to initialize the data and update the placeholder text when updating the content.
When using Method Swizzling, always execute in the +load Method to avoid multiple executions.
2.1. Use the Runtime switching method
/// switch methods
/** * use the runtime switch method */
+ (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;
}
Copy the code
2.2. Swap methods in +load
Swap -init, -settext:, etc
+ (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
{
// here we use self to report an error, so we define a variable 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];
}
Copy the code
Note that self setNeedsDisplay is not self setNeedsLayout.
3. Monitor text changes to update the layout
When the text content changes, execute [self setNeedsDisplay] and call -drawRect: to update the layout and determine whether to display placeholder text.
Add listeners when init and remove listeners when 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];
}
Copy the code
4, the use of
And then you can just use it.
self.textView = [[UITextView alloc] init];
self.textView.placeHolder = @"say something";
self.textView.placeHolderTextColor = [UIColor lightGrayColor];
Copy the code
The advantage of using a category is that it is less intrusive to the original class, just adding two additional attributes and writing the rest as you wish. So is this the reason why I don’t want to learn RAC and SWIFT, learn a whole new set of things, so tired… Lazy cancer is terminal and incurable.
5, source code affixed
5.1, UITextView + YLPlaceHolder. H
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UITextView (YLPlaceHolder)
/// placeholder text color
@property (nonatomic, strong) UIColor *placeHolderTextColor;
/// placeholder text
@property (nonatomic, copy ) NSString * _Nullable placeHolder;
@end
NS_ASSUME_NONNULL_END
Copy the code
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 error 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; // Get the index of the original method in the method list}}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];
}
Copy the code
5.3, NSObject + MethodSwizzling. H
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (MethodSwizzling)
// Switch methods
+ (BOOL)hookOrigInstanceMethod:(SEL)oriSEL newInstanceMethod:(SEL)swizzledSEL;
@end
NS_ASSUME_NONNULL_END
Copy the code
5.4, NSObject + MethodSwizzling. M
#import "NSObject+MethodSwizzling.h"
#import <objc/message.h>
@implementation NSObject (MethodSwizzling)
/// switch methods
/** * use the runtime switch method */
+ (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;
}
Copy the code