基于UIView实现UIKeyInput协议来实现输入框 TextView

news/2024/7/20 20:19:10 标签: ios, objective-c, xcode

之前有次需求是要求做cell内的输入框动态换行,顺便研究了一下UITextView的实现,核心是UIKeyInput协议,重写

- (BOOL)hasText {
- (void)insertText:(NSString *)text {
- (void)deleteBackward {

这三个方法来完成输入框。第一个方法判断是否有文字,第二个是插入文本是的回调,第三个方法是删除按钮按下时的回调。

整个输入框通过SGEditViewDelegate.hSGEditView.hSGEditView.m来实现。

SGEditViewDelegate.h

这个代理作用是模仿UITextView的部分协议方法,例如- (void)editViewDidChange:等等,在合适的时机调用。

#import <Foundation/Foundation.h>

@class SGEditView;

/// Delegate for SGEditView, response to various event called.
@protocol SGEditViewDelegate <NSObject>

/// When EditView did change its text and executed this method.
- (void)editViewDidChange: (SGEditView *)editView;

@optional

/// When EditView
- (BOOL)editViewShouldReturn: (SGEditView *)editView inChange: (NSString *)text;

/// When EditView become first responsder and the Keyboard was presented into scene, executed this method.
- (void)editViewDidBeginEditing: (SGEditView *)editView;

/// When EditView processd all tasks after the Keyboard finished typing. executed this method.
- (void)editViewDidEndEditing: (SGEditView *)editView;


@end

SGEditView.h

对外暴露一些属性,例如占位文字,文字间隙等等

#import <UIKit/UIKit.h>
#import "SGEditViewDelegate.h"

NS_ASSUME_NONNULL_BEGIN

/// Self define EditView likes UITextView but based on UIView.
@interface SGEditView : UIView

/// Delegate target.
@property (nonatomic, weak) NSObject<SGEditViewDelegate> *delegate;

@property (nonatomic, strong) NSString *placeholder;

@property (nonatomic, strong) NSString *text;

@property (nonatomic, strong) UIColor *textColor;

@property (nonatomic, strong) UIFont *font;

@property (nonatomic, assign) UIEdgeInsets textContainerInset;

@property (nonatomic, assign) BOOL isEnableEnterToLeave;

@end

NS_ASSUME_NONNULL_END

SGEditView.m

这里具体实现有些复杂,例如光标的位置计算就实现的有些问题,如果有读者平常业务是frame编写的就知道计算多行文字的末尾position是有多复杂,希望有经验的读者不吝赐教解决这个问题。

系统的UITextView输入框可以输入中文,但是中文存在候选文本,处理起来比较麻烦所以这里就没有实现。

具体实现原理就是:

  1. touchBegin里面注册第一响应者调起键盘;
  2. insertText方法里面在合适的时机把代理方法回调出去;
  3. 使用属性把文本保存起来,例如这里是用_text保存的;
  4. 调用[self setNeedsDisplay];drawRect唤起;
  5. drawRect里面使用CF相关内容绘制文字,最后别忘了由于是调用的c代码,不要忘了手动释放内存。
#import "SGEditView.h"
#import <CoreText/CoreText.h>

static NSString * const path_text = @"text";

static NSString * const path_cursor_opacity = @"opacity";

@interface SGEditView ()<UIKeyInput>

@property (nonatomic, strong) UILabel *placeholderLabel;

@property (nonatomic, strong) UIView *cursor;

@property (nonatomic, strong) CABasicAnimation *cursorAnimation;

@property (nonatomic, readonly) BOOL canBecomeFirstResponder;

@end

@implementation SGEditView

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

- (void)dealloc {
    [self removeObserver:self forKeyPath:path_text context:nil];
}

- (void)setupAttributes {
    self.text = @"";
    [self addSubview:self.placeholderLabel];
    [self addObserver:self forKeyPath:path_text options:NSKeyValueObservingOptionNew context:nil];
    
    [self addSubview:self.cursor];
    self.cursor.frame = CGRectMake(0, 0, 2, self.font.lineHeight);
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if (self.canBecomeFirstResponder){
        [self becomeFirstResponder];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:path_text]) {
        self.placeholderLabel.hidden = self.text.length > 0;

    }
}

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    
    CGRect insetRect = CGRectMake(rect.origin.x + self.textContainerInset.left,
                                  rect.origin.y - self.textContainerInset.top,
                                  rect.size.width - self.textContainerInset.left - self.textContainerInset.right,
                                  rect.size.height  - self.textContainerInset.bottom);
    
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    NSDictionary *attrDictionary = @{NSFontAttributeName: self.font,
                                     NSForegroundColorAttributeName: self.textColor
                                    };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.text attributes:attrDictionary];
    
    CFAttributedStringRef drawStr = CFBridgingRetain(attrString);
    CTFramesetterRef setter = CTFramesetterCreateWithAttributedString(drawStr);
    CGPathRef path = CGPathCreateWithRect(insetRect, NULL);
    CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, CFAttributedStringGetLength(drawStr)), path, NULL);
    
    CGContextSaveGState(ctx);
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -CGRectGetHeight(insetRect));
    CTFrameDraw(frame, ctx);
    CGContextRestoreGState(ctx);
    
    CGPathRelease(path);
    CFRelease(frame);
    CFRelease(setter);
    CFRelease(drawStr);
}

- (BOOL)hasText{
    return self.text.length > 0;
}

- (void)insertText:(NSString *)text {
    if ([self.delegate respondsToSelector:@selector(editViewDidBeginEditing:)]){
        [self.delegate editViewDidBeginEditing:self];
    }
    
    if ([self.delegate respondsToSelector:@selector(editViewShouldReturn:inChange:)]){
        if ([self.delegate editViewShouldReturn:self inChange:text] == YES){
            return;
        }
    }
    
    if ([self.text isEqualToString:@"\n"]){
        [self resignFirstResponder];
        [self setNeedsDisplay];
        return;
    }
    
    self.text = [self.text stringByAppendingString:text];
    
    CGPoint cursorPoint = [self getLiveTextLeadingLocationIn:self.text];
    self.cursor.frame = CGRectMake(cursorPoint.x, cursorPoint.y, self.cursor.frame.size.width, self.cursor.frame.size.height);
    
    if ([self.delegate respondsToSelector:@selector(editViewDidChange:)]){
        [self.delegate editViewDidChange:self];
    }
    [self setNeedsDisplay];
    
    if ([self.delegate respondsToSelector:@selector(editViewDidEndEditing:)]){
        [self.delegate editViewDidEndEditing:self];
    }
}

- (void)deleteBackward {
    if (self.text.length > 0) {
        self.text = [self.text substringToIndex:[self.text length] -1];
    } else {
        self.text = @"";
    }
    
    CGPoint cursorPoint = [self getLiveTextLeadingLocationIn:self.text];
    self.cursor.frame = CGRectMake(cursorPoint.x, cursorPoint.y, self.cursor.frame.size.width, self.cursor.frame.size.height);
    
    [self setNeedsDisplay];
}

- (void)setPlaceholder:(NSString *)placeholder {
    self.placeholderLabel.text = placeholder;
}

- (UILabel *)placeholderLabel {
    if (!_placeholderLabel){
        _placeholderLabel = [[UILabel alloc] init];
        _placeholderLabel.textColor = [UIColor lightGrayColor];
        _placeholderLabel.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), 20);
        
    }
    return _placeholderLabel;
}

- (UIFont *)font {
    if (!_font){
        _font = [UIFont systemFontOfSize:16 weight:UIFontWeightLight];
    }
    return _font;
}

- (UIColor *)textColor {
    if (!_textColor){
        _textColor = [UIColor blackColor];
    }
    return _textColor;
}

- (UIView *)cursor {
    if (!_cursor){
        _cursor = [[UIView alloc] init];
        _cursor.layer.cornerRadius = 1;
        _cursor.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.4];
    }
    return _cursor;
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (CGPoint)getLiveTextLeadingLocationIn: (NSString *)text {
    const CGFloat MAX_SINGLE_W = CGRectGetWidth(self.frame) - self.textContainerInset.left - self.textContainerInset.right;
    const CGFloat FONT_H = ceil(self.font.lineHeight);
    
    CGFloat finalH = [self fixWidthGetHeight:text
                                     andFont:self.font
                                     andFixW:MAX_SINGLE_W] - FONT_H;
    CGFloat realW = [self fixHeightGetWidth:text
                                    andFont:self.font
                                    andFixH:FONT_H];
    
    CGFloat reminderW = reminder(realW, MAX_SINGLE_W);
    CGFloat finalW = realW > MAX_SINGLE_W ? (reminderW + 3.5) : realW;
    return CGPointMake(finalW, finalH);
}

- (CGFloat)fixWidthGetHeight: (NSString *) string andFont: (UIFont *)font andFixW: (CGFloat)w{
    return [string boundingRectWithSize:CGSizeMake(w, CGFLOAT_MAX)
                                options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
                             attributes:@{ NSFontAttributeName: font }
                                context:nil].size.height;
}

- (CGFloat)fixHeightGetWidth: (NSString *) string andFont: (UIFont *)font andFixH: (CGFloat)h{
    return [string boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, h)
                                options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
                             attributes:@{ NSFontAttributeName: font }
                                context:nil].size.width;
}

int reminder(CGFloat a, CGFloat b){
    int c = a;
    int d = b;
    return c - (c / d) * d;
}

@end

ViewController中调用:

import UIKit

class ViewController: UIViewController, SGEditViewDelegate{
    

    lazy var editView: SGEditView = {
        let edit = SGEditView(frame: CGRect(x: 20, y: 200, width: 375 - 40, height: 100))
        edit.backgroundColor = .gray
        edit.placeholder = "Input accout."
        edit.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 5, right: 10)
        return edit
    }()
    

    override func viewDidLoad() {
        super.viewDidLoad()


        self.view.addSubview(editView)
        editView.delegate = self
    }
    
    func editViewDidBeginEditing(_ editView: SGEditView!) {
        print("------> editViewDidBeginEditing: \(editView.text)")
    }
    
    func editViewShouldReturn(_ editView: SGEditView!, inChange text: String!) -> Bool {
        if editView.text.contains("abc") {
            return true
        }
        return false
    }
    
    
    func editViewDidChange(_ editView: SGEditView!) {
        print("------> editViewDidChange: \(editView.text)")
    }
    
    func editViewDidEndEditing(_ editView: SGEditView!) {
        print("------> editViewDidEndEditing: \(editView.text)")
    }
    
}

http://www.niftyadmin.cn/n/111002.html

相关文章

webpack高级配置

摇树&#xff08;tree shaking&#xff09; 我主要是想说摇树失败的原因&#xff08;tree shaking 失败的原因&#xff09;&#xff0c;先讲下摇树本身效果 什么是摇树&#xff1f; 举个例子 首先 webpack.config.js配置 const webpack require("webpack");/**…

问题解决:java.net.SocketTimeoutException: Read timed out

简单了解Sockets Sockets&#xff1a;两个计算机应用程序之间逻辑链接的一个端点&#xff0c;是应用程序用来通过网络发送和接收数据的逻辑接口 是IP地址和端口号的组合每个Socket都被分配了一个用于标识服务的特定端口号基于连接的服务使用基于tcp的流Sockets Java为客户端…

超详细Eclipse配置JDK

在此附上Eclipse安装教程 超详细Eclipse安装教程 在此附上JDK1.8安装配置教程 超详细JDK1.8安装与配置 ①打开Eclipse–>点击Window–>点击Preferences ②找到Java–>找到Installed JREs–>点击Add… ③选中Standard VM–>之后点击Next ④点击Directory找…

Hive中的基础函数(一)

一、hive中的内置函数根据应用归类整体可以分为8大种类型。 1、 String Functions 字符串函数 主要针对字符串数据类型进行操作&#xff0c;比如下面这些&#xff1a; 字符串长度函数&#xff1a;length •字符串反转函数&#xff1a;reverse •字符串连接函数&#xff1a;…

jQuery 事件

jQuery 事件 Date: February 28, 2023 Sum: jQuery事件注册、处理、对象 目标&#xff1a; 能够说出4种常见的注册事件 能够说出 on 绑定事件的优势 能够说出 jQuery 事件委派的优点以及方式 能够说出绑定事件与解绑事件 jQuery 事件注册 单个时间注册 语法&#xff1a;…

机器学习与目标检测作业(数组相加:形状需要满足哪些条件)

机器学习与目标检测&#xff08;数组相加:形状需要满足哪些条件&#xff09;机器学习与目标检测&#xff08;数组相加:形状需要满足哪些条件&#xff09;一、形状相同1.1、形状相同示例程序二、符合广播机制2.1、符合广播机制的描述2.2、符合广播机制的示例程序机器学习与目标检…

什么是CISP-PTE认证?

什么是CISP-PTE认证&#xff1f; 注册信息安全专业人员&#xff08;CISP&#xff09;–渗透测试工程师&#xff08;PTE&#xff09;/渗透测试专家&#xff08;PTS&#xff09;。CISP-PTE认证&#xff0c;证书持有人员主要从事信息安全技术领域网站渗透测试工作&#xff0c;具有…

哪些工具可以实现在线ps的需求

在线Photoshop有哪些工具可以选择&#xff1f;在 Adobe 的官网上就能够实现&#xff0c;很惊讶吧&#xff0c;其实 Adobe 官方推出了在线版本的 Photoshop 的&#xff0c;尽管目前还是 Beta版本&#xff0c;但其实也开放了蛮久了。编辑切换为居中添加图片注释&#xff0c;不超过…