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