最近在做app時(shí),需要內(nèi)置一個(gè)富文本編輯器,調(diào)研之后采用webView +editable html方案,寫demo時(shí)看起來挺簡(jiǎn)單,毫不猶豫得開始搞,卻發(fā)現(xiàn)在真正集成進(jìn)app時(shí)卻踩坑不少,真是啪啪打臉~
需求如下:在鍵盤上方,點(diǎn)擊按鈕時(shí)可以隨意切換到各種選擇區(qū),并保證webView不失焦,如下圖所示:
調(diào)研過程:
1. 最先想到得方案便是,點(diǎn)擊按鈕,關(guān)閉鍵盤,然后彈出自定義試圖。
比如表情選擇器。這種方案當(dāng)然可行,但比較麻煩,當(dāng)選擇完一個(gè)表情,此時(shí)html已經(jīng)處于失焦?fàn)顟B(tài),是沒辦法直接輸入進(jìn)html的,當(dāng)然你可以記錄失焦前的位置,然后使用document.exeCommand方法硬插入,同時(shí)還有一個(gè)問題是,切換試圖效果并不流暢,會(huì)有鍵盤關(guān)閉得過程,本來直接就可以輸入的,就因?yàn)榍袚Q鍵盤不能輸了,忍痛舍棄。
2. 接下來的想法便是,如何能讓切換鍵盤時(shí)不失焦。查看官方文檔獲得如下知識(shí)
2.1 keboard實(shí)際上是一個(gè)inputView,作為UIResponder類的屬性,每一個(gè)子類都是可以定制它的,只要重新定義inputView property為讀寫屬性,并重寫getter方法即可,系統(tǒng)本身可定制的UITextField和UITextView就是這么干的
This property is typically used to provide a view to replace the system-supplied keyboard that is presented for UITextField and UITextView objects.
The value of this read-only property is nil. A responder object that requires a custom view to gather input from the user should redeclare this property as read-write and use it to manage its custom input view. When the receiver becomes the first responder, the responder infrastructure presents the specified input view automatically.
2.2 若當(dāng)前試圖為第一響應(yīng)者,可以調(diào)用UIResponder的 reloadInputViews方法,強(qiáng)制刷新inputView以及inputAccessView。
You can use this method to refresh the custom input view or input accessory view associated with the current object when it is the first responder. The views are replaced immediately—that is, without animating them into place. If the current object is not the first responder, this method has no effect.
2.3 有了如上知識(shí),思路就有了,那修改UIWebView的inputView就可以了,操作之后發(fā)現(xiàn),鍵盤是可以自由切換得,但是失焦了,那也就是說輸入狀態(tài),webView不是第一響應(yīng)者,那肯定是內(nèi)部得子試圖了,最終通過代碼打印第一響應(yīng)者,發(fā)現(xiàn)其是一個(gè)叫UIWebBrowser的東西,這貨是繼承于UIView的,但是是內(nèi)部私有類,怎么辦?browser信息如下
3. 是時(shí)候使用runtime了。
3.1 要么替換掉UIWebBrowser類為我們自定義類;要么替換該類的inputView getter方法,然后在切換切換鍵盤時(shí)修改inputView即可,我采用第一種方案,這也符合蘋果官方文檔的做法,核心代碼如下:
- (void)_hackWebBrowserClass{
// 此處想法是暫存最開始的inputView,實(shí)際證明并不需要
// if ([UIWebViewHackishlyViewManager sharedInstance].originalKeyboardView == nil){
// [UIWebViewHackishlyViewManager sharedInstance].originalKeyboardView = self.inputView;
// }
UIView *browserView = [self browserView];
Class hackClass = objc_getClass(hackishFixClassName);
// 如果還未生成自定義class,則生成;就是說還沒替換
if (!hackClass) {
hackClass = [self _hackishSubclassExists];
}
if (![browserView isMemberOfClass:hackClass]){
if (hackClass){
object_setClass(browserView, hackClass);
}
}
}
// 生成自定義類,其繼承于UIWebBrowser類
- (Class)_hackishSubclassExists {
if (objc_getClass(hackishFixClassName)) return objc_getClass(hackishFixClassName);
Class newClass = objc_allocateClassPair([[self browserView] class], hackishFixClassName, 0);
// 添加兩個(gè)getter方法;使用replace應(yīng)該也是可以的,可以試試
IMP accessoryViewImp = [self methodForSelector:@selector(changedInputAccessoryView)];
class_addMethod(newClass, @selector(inputAccessoryView), accessoryViewImp, "@@:");
IMP inputViewImp = [self methodForSelector:@selector(changedInputView)];
class_addMethod(newClass, @selector(inputView), inputViewImp, "@@:");
objc_registerClassPair(newClass);
return newClass;
}
3.2 中間繞了一個(gè)彎。當(dāng)需要切換回系統(tǒng)鍵盤時(shí),本來想把UIWebBrowser的實(shí)現(xiàn)替換為系統(tǒng)本身得實(shí)現(xiàn),但是這么干的話,可惡的inputAccessryView也隨著鍵盤一塊兒出來了,所以不能把類替換回去,那怎么辦呢?最終采用保存原始的inputView方式,當(dāng)需要切換回系統(tǒng)鍵盤時(shí),替換回去 【后來證明不需要這么做,如下代碼多余了】
代碼如下:
- (UIView*)changedInputView{
UIView *view = [UIWebViewHackishlyViewManager sharedInstance].inputView;
if(view)returnview;
return [UIWebViewHackishlyViewManager sharedInstance].originalKeyboardView;
}
完整demo待整理后發(fā)出
小總結(jié):
經(jīng)此一役,再次感受到runtime的強(qiáng)大之處,可隨意修改私有類,私有方法(當(dāng)然也要不影響方法本身功能,慎用)