UITextView自增高,类似微信输入框的效果

前言

最近在看以前的代码的时候,发现自增高的实现有点复杂。在计算高度的时候有些数值是自己估摸着实现的,反正代码看着很不友好,就想着重构一下。完整测试代码在文章最下方!

那么要实现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..

下面是打印出来的结果

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")

这个系统自带的placeholderLabelUITextFieldplaceholder一样。你只要自己写个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时,通知中心对Observerunsafe_unretained引用,而iOS9.0以后,通知中心对Observer实现了weak引用,这两个引用的区别在与,weak在对象释放掉之后会置nil,而unsafe_unretained在对象释放掉之后会变成野指针,所以需要在对象释放掉之前将Observer移除,防止野指针通信,造成Crash

Discussion

这段代码有一个致命的缺陷是不支持设置contentInset属性,若是设置了contentInset文字会上下跳,有兴趣的同学可以在Demo里面测试一下,要是能解决这个问题就再好不过了,谢谢大家啦!
本文Demo仅供交流使用,切勿直接扔进项目里!

你可能感兴趣的:(UITextView自增高,类似微信输入框的效果)