iOS如何自定义一个类似UITextView的本文编辑View

news2025/1/19 17:03:44

对于IOS涉及文本输入常用的两个View是UITextView和UITextField,一个用于复杂文本输入,一个用于简单文本输入,在大多数开发中涉及文本输入的场景使用这两个View能够满足需求。但是对于富文本编辑相关的开发,这两个View就无法满足自己业务的需要了。

在多数情况下,设计富文本开发的业务,大多需要根据键盘输入的内容根据业务场景对文本进行布局排版,这方面有个开源的富文本编辑View那就是YYText,网上也有很多对其源码分析的文章,这里就不过多介绍了,这个自定义的YYTextView堪称大神级,性能和效果都很不错,代码质量相当高,可以细细研读。但是这个代码相对比较复杂,对于研究如何自定义文本编辑View不太友好。然而苹果官方提供了一个简单版自定义文本编辑View的开源项目SimpleTextInput,这个简单易理解,下面就基于此说下如何自定义一个文本编辑的view

对于文本编辑而言,设计两个关键点:1、和键盘交互,当点击键盘上的按键时屏幕上如何显示;2、内容排版,屏幕上的内容如何布局显示。

对于第一点,iOS提供了两个相应协议:1、UIKeyInput简单的键盘交互

2、UITextInput复杂的键盘交互,可以看出UITextInput继承UIKeyInput,因此包含了UIKeyInput所有接口

UIKeyInput只能输入字符,因此提供的接口也简单,只有是否有文本内容,插入、删除三个接口

UITextInput用于输入点击键盘时屏幕不是显示最终内容时,比如输入汉字时,在输入拼音时,屏幕显示的是拼音字符有个蓝色底色,只有最终选中汉字时会将蓝色底色包围的拼音替换为汉字

对比只继承UIKeyInput的view继承UITextInput的view键盘多了红框选字的部分。

那么如何使用这两个协议呢,很简单,基于UIView创建一个自定义View子类,继承UIKeyInput或者UITextInput

@interface APLEditableCoreTextView : UIView <UITextInput> 


@end

为了使自定义的view能够接受输入需要重载方法canBecomeFirstResponder返回YES这样自定义view才能相应输入

如果是继承UIKeyInput就只需实现

@implementation APLEditableCoreTextView

- (BOOL)canBecomeFirstResponder
{
    return YES;
}

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

- (void)insertText:(NSString *)text
{


}

- (void)deleteBackward
{
   
}

@end

这三个接口就可以了,当点自定义的view处于第一响应时,系统会触发hasText先判断是否有内容,当点击键盘字母时会调insertText:(NSString*)text方法,当点击键盘delete键会触发deleteBackward方法

如果是继承UITextInput需要实现的就比较多了,这个协议的@required下所有方法都必须实现再加上UIKeyInput的三个方法

/* Methods for manipulating text. */
// 根据给定的范围返回一个字符串,一般是屏幕上已有的内容取range范围内的子字符串
- (nullable NSString *)textInRange:(UITextRange *)range;
// 用给定的字符串替换给定范围部分,一般是屏幕上已有内容range范围内替换为text
- (void)replaceRange:(UITextRange *)range withText:(NSString *)text;

/* Text may have a selection, either zero-length (a caret) or ranged.  Editing operations are
 * always performed on the text from this selection.  nil corresponds to no selection. */

// 已选的范围
@property (nullable, readwrite, copy) UITextRange *selectedTextRange;

/* If text can be selected, it can be marked. Marked text represents provisionally
 * inserted text that has yet to be confirmed by the user.  It requires unique visual
 * treatment in its display.  If there is any marked text, the selection, whether a
 * caret or an extended range, always resides within.
 *
 * Setting marked text either replaces the existing marked text or, if none is present,
 * inserts it from the current selection. */ 

// 标记的文本范围,如果没有标记文本返回nil,标记文本可以理解为输入内容时,屏幕上蓝色包裹的字符串
@property (nullable, nonatomic, readonly) UITextRange *markedTextRange; // Nil if no marked text.
// 标记文本如何显示
@property (nullable, nonatomic, copy) NSDictionary<NSAttributedStringKey, id> *markedTextStyle; // Describes how the marked text should be drawn.
// 用给定范围设置标记文本内容
- (void)setMarkedText:(nullable NSString *)markedText selectedRange:(NSRange)selectedRange; // selectedRange is a range within the markedText
// 解除标记文本,可以理解为用键盘内容替换蓝色包裹部分,比如选中键盘上汉字替换为屏幕上拼音时,拼音就是要解除的标记文本
- (void)unmarkText;

/* The end and beginning of the the text document. */
// 文本开始位置,一般是0
@property (nonatomic, readonly) UITextPosition *beginningOfDocument;
// 文本结束位置,一般是屏幕文本长度
@property (nonatomic, readonly) UITextPosition *endOfDocument;

/* Methods for creating ranges and positions. */
// 用给定的开始和结束位置,返回一个range
- (nullable UITextRange *)textRangeFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition;
// 用给定位置和偏移量返回一个新的位置
- (nullable UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset;
// 用给定的位置、方向和偏移量返回一个新的位置
- (nullable UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset;

/* Simple evaluation of positions */
// 比较两个位置关系
- (NSComparisonResult)comparePosition:(UITextPosition *)position toPosition:(UITextPosition *)other;
// 返回给定起始位置和结束位置的偏移量
- (NSInteger)offsetFromPosition:(UITextPosition *)from toPosition:(UITextPosition *)toPosition;

/* A system-provided input delegate is assigned when the system is interested in input changes. */
// 输入代理,这个代理不需要自己继承UITextInputDelegate协议定义一个类,系统会给其分配一个实例的
@property (nullable, nonatomic, weak) id <UITextInputDelegate> inputDelegate;

/* A tokenizer must be provided to inform the text input system about text units of varying granularity. */
// 分词器,需要给其创建,如果没有特殊需求直接使用UITextInputStringTokenizer就可以了
@property (nonatomic, readonly) id <UITextInputTokenizer> tokenizer;

/* Layout questions. */
// 根据给定的布局范围和方向返回一个最远的新位置
- (nullable UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction;
// 根据给定的位置和方向返回一个字符的范围
- (nullable UITextRange *)characterRangeByExtendingPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction;

/* Writing direction */
// 根据给的位置和记录方向返回书写方向
- (NSWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction;
// 设置指定范围内的指定的书写方向
- (void)setBaseWritingDirection:(NSWritingDirection)writingDirection forRange:(UITextRange *)range;

/* Geometry used to provide, for example, a correction rect. */
// 返回给定范围内的rect
- (CGRect)firstRectForRange:(UITextRange *)range;
// 返回给定位置的光标rect,一般输入框都会有个闪烁的竖线,这个竖线其实是个矩形,为了能让这个竖线在文本间显示且不与文字重叠,需要给字与字之间留够空隙,这个空隙是文字布局时做到,但是空隙大小是通过这个方法得到的
- (CGRect)caretRectForPosition:(UITextPosition *)position;
// 返回给定范围内的选区,这是个数组,因为选区内可能有个多个rect
- (NSArray<UITextSelectionRect *> *)selectionRectsForRange:(UITextRange *)range API_AVAILABLE(ios(6.0));       // Returns an array of UITextSelectionRects

/* Hit testing. */
// 返回给定点的最近位置
- (nullable UITextPosition *)closestPositionToPoint:(CGPoint)point;
// 返回给定范围内指定点的最近位置
- (nullable UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange *)range;
// 返回给定点出的文本范围,一般是用于分词,比如在“今天天气不错”这个内容的“气”字位置处可能想要的的是“天气”这个词的范围
- (nullable UITextRange *)characterRangeAtPoint:(CGPoint)point;

这些方法的触发都是在与键盘交互时相应的。其中UITextInput有个属性inputDelegate,这个不需要设置,由系统设置分配的UIKeyboardImpl实例

通过调用堆栈可以看出,当设置view为第一响应者时,会给inputDelegate设置一UIKeyboardImpl的实例

其中tokenizer属性需要给其分配实例,self.tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];用户给键盘使用,实际view中并不怎么用这个属性

以上就是关于和键盘交互的所有说明,记住UIKeyInput/UITextInput的接口都是有键盘触发的,可以在这些接口实现断点调试看下堆栈,比如hasText,当点击键盘时会立即触发

再比如markedTextRange

等等,通过堆栈发现,执行UITextInput的接口时,都是通过UIKeyboardImpl这个实例最终执行到UITextInput的接口实现,这个UIKeyboardImpl就是inputDelegate的实例,这个UITextInputDelegate协议有四个接口

我们在selectedTextRange方法处断点调试,通过堆栈可以看出,这个方法就是inputDelegate执行selectionDidChange最终执行了selectedTextRange

最后我们看下UITextInput中涉及到的两个类UITextPosition和UITextRange

@interface UITextPosition : NSObject

@end

@interface UITextRange : NSObject

@property (nonatomic, readonly, getter=isEmpty) BOOL empty;     //  Whether the range is zero-length.
@property (nonatomic, readonly) UITextPosition *start;
@property (nonatomic, readonly) UITextPosition *end;

@end

通过定义可以看出,UITextPosition是个空类,没有任何变量和属性,通过类名可以知道这是个定义位置的类,但是并没有变量和属性如何知道位置的值呢。同时UITextRange是一个选区类,有个属性empty显然是为了说明选区是否为空,但是并没有实现,这两个类都只给了定义并没有实现。因此需要开发者继承这两个类自定义UITextPosition和UITextRange。

@interface APLIndexedPosition : UITextPosition

@property (nonatomic) NSUInteger index;
@property (nonatomic) id <UITextInputDelegate> inputDelegate;

+ (instancetype)positionWithIndex:(NSUInteger)index;

@end

@implementation APLIndexedPosition

#pragma mark IndexedPosition implementation

// Class method to create an instance with a given integer index.
+ (instancetype)positionWithIndex:(NSUInteger)index
{
    APLIndexedPosition *indexedPosition = [[self alloc] init];
    indexedPosition.index = index;
    return indexedPosition;
}

@end
@interface APLIndexedRange : UITextRange

@property (nonatomic) NSRange range;
+ (instancetype)indexedRangeWithRange:(NSRange)range;

@end

@implementation APLIndexedRange

// Class method to create an instance with a given range
+ (instancetype)indexedRangeWithRange:(NSRange)range
{
    if (range.location == NSNotFound) {
        return nil;
    }

    APLIndexedRange *indexedRange = [[self alloc] init];
    indexedRange.range = range;
    return indexedRange;
}


// UITextRange read-only property - returns start index of range.
- (UITextPosition *)start
{
    return [APLIndexedPosition positionWithIndex:self.range.location];
}


// UITextRange read-only property - returns end index of range.
- (UITextPosition *)end
{
	return [APLIndexedPosition positionWithIndex:(self.range.location + self.range.length)];
}


// UITextRange read-only property - returns YES if range is zero length.
-(BOOL)isEmpty
{
    return (self.range.length == 0);
}

这就是在selectedTextRange方法中返回的是APLIndexedRange的原因

以上是如何键盘交互,对于已经在屏幕上的内容,如何布局排版显示,涉及文字绘制,可以参考官方文档Core Text这是一个纯C语言的较为底层的文本排版渲染的框架,其中UITextInput中- (CGRect)caretRectForPosition:(UITextPosition *)position接口就涉及core text相关方法来计算字间距,这里就不对此进行详细阐述了,网上对core text详解有很多,而对UITextInput的使用较少,这里对此做详细说明

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2255962.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《黑神话:悟空》闪退,提示D3D12崩溃,游戏崩溃无法启动是什么原因?要怎么解决?

《黑神话&#xff1a;悟空》闪退、D3D12崩溃及游戏无法启动&#xff1a;原因、解决方案与预防措施 作为一名软件开发从业者&#xff0c;我深知电脑游戏运行时可能遇到的各种问题&#xff0c;尤其是像《黑神话&#xff1a;悟空》这样的高品质游戏&#xff0c;其对硬件和系统配置…

JUC:Synchronized和锁升级

1. 面试题 谈谈你对Synchronized的理解Sychronized的锁升级你聊聊Synchronized实现原理&#xff0c;monitor对象什么时候生成的&#xff1f;知道monitor的monitorenter和monitorexit这两个是怎么保证同步的嘛&#xff1f;或者说这两个操作计算机底层是如何执行的偏向锁和轻量级…

SAP SD学习笔记19 - 形式发票(Proforma Invoice)

上面几章讲了投诉处理。 SAP SD学习笔记18 - 投诉处理4 - 请求书订正依赖&#xff0c;投诉处理流程的总结-CSDN博客 本章继续学习SD 模块的其他内容。 本章讲了形式发票&#xff08;Proforma Invoice&#xff09;的概要及系统操作。 形式发票是在出库确认之前&#xff0c;有…

M005 PHP+MYSQL+web编程课程网站的设计与实现 源码 配置 文档

web编程课程网站 1.摘要2.开发目的和意义3.系统功能设计4.系统界面截图5.源码获取 1.摘要 随着互联网的飞速发展&#xff0c;各行各业的信息化进程逐步加快。商业信息化、政务信息化、教育信息、服务信息化等等已遍布全国各地。信息化的服务平台能更加高效的为用户提供各种服务…

【力扣】13.罗马数字转整数

问题描述 思路解析 对于这种限制字符的问题&#xff0c;使用Map来对键值存储 对其进行判断&#xff0c;如果前面的数小于后面的数&#xff0c;那么结果相减 否则&#xff0c;正常相加。 代码 class Solution {Map<Character,Integer> mapnew HashMap<Character,In…

docker安装ddns-go(外网连接局域网)

docker先下载镜像&#xff0c;目前最新版是v6.7.6 也可以csdn资源下载 再导入dockers https://download.csdn.net/download/u014756339/90096748 docker load -i ddns-go.tar 启动 docker run -d --name ddns-go --restartalways --nethost -v /opt/ddns-go:/root jeessy/…

洛谷P4913 【深基16.例3】二叉树深度(c嘎嘎)

题目链接&#xff1a;P4913 【深基16.例3】二叉树深度 - 洛谷 | 计算机科学教育新生态 题目难度&#xff1a;普及 解题思路&#xff1a;本题要求树的深度&#xff0c;即求左右子树高度的最大值&#xff0c;首先我们用结构体存树左右节点&#xff0c;然后分别递归地去左右子树的…

Android -- [SelfView] 自定义多行歌词滚动显示器

Android – [SelfView] 自定义多行歌词滚动显示器 流畅、丝滑的滚动歌词控件* 1. 背景透明&#xff1b;* 2. 外部可控制进度变化&#xff1b;* 3. 支持屏幕拖动调节进度&#xff08;回调给外部&#xff09;&#xff1b;效果 歌词文件&#xff08;.lrc&#xff09; 一. 使用…

DNS/域名

概述 每个应用层协议都是为了解决某一类应用问题&#xff0c;而问题的解决又往往是通过位于不同主机中的多个应用进程之间的通信和协同工作来完成的。应用层的具体内容就是规定应用进程在通信时所遵循的协议。 应用层的许多协议都是基于客户服务器方式。客户(client)和服务器…

淘宝直播间智能化升级:基于LLM的学习与分析

自营直播应用技术团队负责的业务中&#xff0c;淘宝买菜的直播业务起步较晚&#xff0c;业务发展压力较大&#xff0c;业务上也就有了期望能够对一些二方的标杆直播间进行学习&#xff0c;并将其优点应用到自己直播间的需求。 最初 - 人海战术&#xff0c;学习PK 业务侧最直接的…

数学拯救世界(一)———寻“数”记

一、 很久很久以前&#xff0c;在一个只认识整数和小数的国度&#xff0c;有一个很残暴的国王提了一个要求&#xff1a;要是不能表示出把一段1米的绳子三等分后的大小&#xff0c;就要把所有的大臣杀掉。 1➗3 0.333&#xff0c;怎么办呀&#xff1f;怎么办呀&#xff1f; 袁q…

夏普MX-4608N复印机维修模式进入方法及载体初始化方法

夏普MX-4608N复印机载体型号&#xff08;图&#xff09;&#xff1a; 型 号&#xff1a;载体&#xff08;黑色&#xff09;MX-561CV 净含量&#xff1a;395克 下面图片中分别有载体、刮板、鼓芯、上纸盒搓纸轮一套&#xff0c;均原装正品&#xff1b; 保养周期将至的时候建…

FPGA Xilinx维特比译码器实现卷积码译码

FPGA Xilinx维特比译码器实现卷积码译码 文章目录 FPGA Xilinx维特比译码器实现卷积码译码1 Xilinx维特比译码器实现2 完整代码3 仿真结果 MATLAB &#xff08;n,k,m&#xff09;卷积码原理及仿真代码&#xff08;你值得拥有&#xff09;_matlab仿真后代码-CSDN博客 MATLAB 仿真…

java+ssm+mysql水产品商城

项目介绍&#xff1a; 使用javassmmysql开发的水产品商城&#xff0c;系统包含管理员、用户角色&#xff0c;功能如下&#xff1a; 管理员&#xff1a;用户管理&#xff1b;种类管理&#xff1b;商品管理&#xff1b;订单管理&#xff1b;评论管理&#xff1b;新闻管理&#…

【传感器技术】第5章 电容式传感器,变极距式电容传感器,变面积式电容传感器,变介质式电容传感器

关注作者了解更多 我的其他CSDN专栏 过程控制系统 工程测试技术 虚拟仪器技术 可编程控制器 工业现场总线 数字图像处理 智能控制 传感器技术 嵌入式系统 复变函数与积分变换 单片机原理 线性代数 大学物理 热工与工程流体力学 数字信号处理 光电融合集成电路…

二叉树前序遍历

什么是前序遍历&#xff1f; 一个二叉树的前序遍历就是对于树中的每一个节点而言&#xff0c;都是先遍历自己&#xff0c;再遍历左右。 如&#xff1a; 递归实现前序遍历 题目链接&#xff1a;144. 二叉树的前序遍历 - 力扣&#xff08;LeetCode&#xff09; 实现步骤&…

RAG系统分类、评估方法与未来方向

分享一篇RAG综述&#xff1a;Retrieval-Augmented Generation for Large Language Models: A Survey&#xff0c;主要想了解一下RAG的评估方法&#xff0c;分享给大家。 文章目录 一、RAG分类二、评估方法三、未来方向 一、RAG分类 RAG分类&#xff1a;Navie RAG、Advanced RA…

【软件安全】软件安全设计规范,软件系统安全设计制度(Word原件)

1.1安全建设原则 1.2 安全管理体系 1.3 安全管理规范 1.4 数据安全保障措施 1.4.1 数据库安全保障 1.4.2 操作系统安全保障 1.4.3 病毒防治 1.5安全保障措施 1.5.1实名认证保障 1.5.2 接口安全保障 1.5.3 加密传输保障 1.5.4终端安全保障 软件全面文档清单涵盖以下核心内容&a…

约克约克VRF中央空调,清凉舒适从此不再是梦

生活总是少不了空调的陪伴。但是&#xff0c;你是否还在为传统空调的高能耗、低效率而烦恼&#xff1f;别担心&#xff0c;约克VRF中央空调来帮你解决这一切难题&#xff01;      节能省电&#xff0c;我懂你~      现代生活讲究的是高效和环保&#xff0c;而约克VRF中…

DMA简介

DMA是一个数据转运小助手, 它主要是用来协助CPU,完成数据转运的工作 第一个程序: 在这个程序里&#xff0c;我们将使用DMA&#xff0c;进行存储器到存储器的数据转运, 也就是把一个数组里面的数据&#xff0c; 复制到另一个数组里 DMA简介 DMA外设&#xff0c; 是可以直接访…