UITextView自增高,類似微信輸入框的效果
前言
最近在看以前的程式碼的時候,發現自增高的實現有點複雜。在計算高度的時候有些數值是自己估摸著實現的,反正程式碼看著很不友好,就想著重構一下。完整測試程式碼在文章最下方!
那麼要實現UITextView輸入框有兩個要點:
- 佔位符
- 自增高
首先我們來看一下
UITextView
這個東西,乍一看,它跟
UITextFiled
好像是一家人,其實他們的父類都不是同一個。
UITextView
是繼承自
UIScrollView
的,而
UITextField
是繼承自
UIControl
的。
Placeholder
做輸入框最基本的就是
placeholder
(佔位符),當然也有些輸入框是沒有佔位符提示的,比如微信,我們不管它,我就是要搞個佔位符。那麼問題來了,
UITextView
是沒有
placeholder
這個屬性的。這就是最蛋疼的地方,你一個輸入框的類,竟然連
placeholder
都沒有,就想
UITextFiled
沒有
textFieldDidChange:
這個方法一樣:cry:!
好吧,那我們就來手動實現一下
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 僅供交流使用,切勿直接扔進專案裡!