前言
最近在看以前的代码的时候,发现自增高的实现有点复杂。在计算高度的时候有些数值是自己估摸着实现的,反正代码看着很不友好,就想着重构一下。完整测试代码在文章最下方!
那么要实现UITextView输入框有两个要点:
- 占位符
- 自增高
首先我们来看一下UITextView
这个东西,乍一看,它跟UITextFiled
好像是一家人,其实他们的父类都不是同一个。UITextView
是继承自UIScrollView
的,而UITextField
是继承自UIControl
的。
Placeholder
做输入框最基本的就是placeholder
(占位符),当然也有些输入框是没有占位符提示的,比如微信,我们不管它,我就是要搞个占位符。那么问题来了,UITextView
是没有placeholder
这个属性的。这就是最蛋疼的地方,你一个输入框的类,竟然连placeholder
都没有,就想UITextFiled
没有textFieldDidChange:
这个方法一样😢!
好吧,那我们就来手动实现一下placeholder
吧。要想添加一个placeholder
其实有很多方法,其中最常用的方法就是给UITextView
上加一个UILabel
,然后在textViewDidChange:
方法里面来控制他的显示和隐藏。那有没有更方便简洁,看起来又比较牛逼的方法呢?有的!
首先,我们来遍历一下UITextView
这个类里的成员变量,用到的是runtime
里的入门小知识
var count: UInt32 = 0
let ivars = class_copyIvarList(UITextView.self, &count)
for i in 0..<count {
let ivar = ivars![Int(i)];
let name = ivar_getName(ivar);
let objcName = String(utf8String: name!)
print(objcName as Any);
}
下面是打印出来的结果
Optional("_private")
Optional("_textStorage")
Optional("_textContainer")
Optional("_layoutManager")
### Optional("_containerView") ###
Optional("_inputDelegate")
Optional("_tokenizer")
Optional("_inputController")
Optional("_interactionAssistant")
Optional("_textInputTraits")
Optional("_autoscroll")
Optional("_tvFlags")
Optional("_contentSizeUpdateSeqNo")
Optional("_scrollTarget")
Optional("_scrollPositionDontRecordCount")
Optional("_scrollPosition")
Optional("_offsetFromScrollPosition")
Optional("_linkInteractionItem")
Optional("_dataDetectorTypes")
Optional("_preferredMaxLayoutWidth")
### Optional("_placeholderLabel") ###
Optional("_inputAccessoryView")
Optional("_linkTextAttributes")
Optional("_streamingManager")
Optional("_characterStreamingManager")
Optional("_siriAnimationStyle")
Optional("_siriAlignment")
Optional("_siriParameters")
Optional("_firstBaselineOffsetFromTop")
Optional("_lastBaselineOffsetFromBottom")
Optional("_intrinsicSizeCache")
Optional("_cuiCatalog")
Optional("_beforeFreezingTextContainerInset")
Optional("_duringFreezingTextContainerInset")
Optional("_beforeFreezingFrameSize")
Optional("_unfreezingTextContainerSize")
Optional("_animatingPaste")
Optional("_frameOfTrailingWhitespace")
Optional("_textDragDropSupport")
Optional("_topContentPadding")
Optional("_bottomContentPadding")
Optional("_scrollEndDraggingVelocity")
Optional("_adjustsFontForContentSizeCategory")
Optional("_clearsOnInsertion")
Optional("_pasteDelegate")
Optional("_multilineContextWidth")
Optional("_textDragOptions")
Optional("_textDragDelegate")
Optional("_textDropDelegate")
Optional("_inputView")
Optional("_visualStyle")
是不是一大堆不知所云的东西,其他的不用管,只要看其实用###
标出来的最关键的两个成员变量_placeholderLabel
和_containerView
。先不管_containerView
,先来看_placeholderLabel
,这不就是占位符吗。不过苹果没有把这个暴露出这个属性给开发者使用,那么我们怎么使用者个私有的成员变量呢?当然使用KVC
了!这个时候发现KVC
是个神器了吧!
setValue(placeHolderLabel, forKey: "_placeholderLabel")
这个系统自带的placeholderLabel
和UITextField
的placeholder
一样。你只要自己写个label
赋值给他就可以了,他的显示和消失有系统控制!
那么placeholder
就搞定了,非常简单吧,一个KVC
轻松解决。
自增高
那么如何来实现自增高呢?这个时候大家应该已经想到了上面打印出来的那个被###
出来的两个成员变量之一_containerView
。顾名思义,这个东西就是个内容视图。你用View UI Hierarchy
查看一下就会惊奇的发现,他的frame是根据文字高度变化的,也就是说_containerView
是自增高的!那么问题就解决了,我们用KVC把这个_containerView
取出来。
let containerView = setValue(placeHolderLabel, forKey: "_containerView")
然后再用KVO
监听containerView
// 注册监听
containerView.addObserver(self, forKeyPath: "frame", options: .new, context: nil)
// 实现监听
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {}
这样我们就完美的实现了UITextView
的自增高,代码又少又简单!
Tips
这里有个关于KVO
的小贴士,苹果在官方文档里已经说明了iOS9.0
以后已经不需要手动removeObserver:
了,除了addObserverForName:object:queue:usingBlock:
方法,因为这个方法在通知中心注册的时候还是强引用的,所以要手动移除。
为什么iOS9.0
以后不需要手动移除Observer
了呢?
因为在iOS9.0
以前,注册Observer
时,通知中心对Observer
做unsafe_unretained
引用,而iOS9.0
以后,通知中心对Observer
实现了weak
引用,这两个引用的区别在与,weak
在对象释放掉之后会置nil
,而unsafe_unretained
在对象释放掉之后会变成野指针,所以需要在对象释放掉之前将Observer
移除,防止野指针通信,造成Crash
。
Discussion
这段代码有一个致命的缺陷是不支持设置contentInset
属性,若是设置了contentInset
文字会上下跳,有兴趣的同学可以在Demo里面测试一下,要是能解决这个问题就再好不过了,谢谢大家啦!
本文Demo仅供交流使用,切勿直接扔进项目里!