前言
先說(shuō)問題是什么,那就是當(dāng)UICollectionView的cell上有webview且在webview中進(jìn)行過(guò)點(diǎn)擊操作后,webview所在cell就無(wú)法觸發(fā)UICollectionView的代理方法willDisplay和didEndDisplaying。
demo鏈接: demo
如何發(fā)現(xiàn)
在做分頁(yè)支持web活動(dòng)頁(yè)的需求時(shí),QA找了一個(gè)全是視頻的鏈接測(cè)試。我們的webview封裝VC在willAppear和willDisappear方法中都會(huì)告訴前端進(jìn)入或者離開頁(yè)面從而達(dá)到離開頁(yè)面暫停播放的效果。而分頁(yè)的框架是由UICollectionView實(shí)現(xiàn)的,其中每個(gè)cell的willAppear和willDisappear方法都是由UICollectionView的willDisplay和didEndDisplaying代理方法來(lái)實(shí)現(xiàn)的。一旦在cell中的webview播放完成視頻,點(diǎn)擊重播后,這個(gè)cell就再也不會(huì)觸發(fā)上述這兩個(gè)方法了,進(jìn)而導(dǎo)致web中的視頻沒法暫停。
在經(jīng)過(guò)排查后發(fā)現(xiàn)這個(gè)問題與視頻無(wú)關(guān),只要在webview中進(jìn)行過(guò)點(diǎn)擊操作后就會(huì)出現(xiàn)。自測(cè)發(fā)現(xiàn)在iOS13上無(wú)論是UIWebView還是WKWebView,都存在這個(gè)問題。QA也在iOS9上發(fā)現(xiàn)相同的問題。
復(fù)現(xiàn)
我們使用兩種cell來(lái)復(fù)現(xiàn)這個(gè)問題,一個(gè)是有帶webview的cell,一種是普通的cell。我們分別在willDisplay和didEndDisplaying代理方法中加入如下代碼
if cell.isKind(of: WebCell.self) {
print("WebCell:willDisplay")
} else {
print("other:willDisplay")
}
在CollectionView中我們?cè)谧笥覂蓚?cè)的cell為普通的cell,中間的cell為攜帶webview的cell,如圖1。

在三個(gè)cell中切換我們會(huì)輸出如下信息
other:willDisplay
WebCell:willDisplay
other:didEndDisplaying
other:willDisplay
WebCell:didEndDisplaying
但一旦在webview中對(duì)按鈕進(jìn)行點(diǎn)擊操作,就會(huì)輸出如下信息
other:willDisplay
other:didEndDisplaying
other:willDisplay
other:didEndDisplaying
other:willDisplay
我們可以發(fā)現(xiàn)這些信息中少了webview的信息,也就意味著webcell走不了willDisplay和didEndDisplaying代理方法。
經(jīng)過(guò)測(cè)試,如果webCell可以被復(fù)用,那么當(dāng)多個(gè)webCell滿足無(wú)法觸發(fā)代理的條件并經(jīng)過(guò)復(fù)用后,有且僅有一個(gè)webCell無(wú)法觸發(fā)willDisplay和didEndDisplaying代理方法。
原因
使用Xcode查看內(nèi)存圖可以發(fā)現(xiàn)正常的持有情況是如圖2的。

但是如果你在webview內(nèi)進(jìn)行了點(diǎn)擊操作后,它的內(nèi)存圖就變成了圖3。

我們可以發(fā)現(xiàn),webCell轉(zhuǎn)而被一個(gè)私有屬性_firstResponderView持有了。我們可以推測(cè)出原因就是這個(gè)webCell原本應(yīng)該在UICollectionView中的數(shù)組里,這樣才可以觸發(fā)willDisplay和didEndDisplaying代理方法,但現(xiàn)在已經(jīng)不再這個(gè)數(shù)組里了,也就無(wú)法觸發(fā)這兩個(gè)代理方法。
解決方案
其實(shí)我們看到webCell被私有屬性_firstResponderView持有后,可以想到把keyWindow的firstResponder取出來(lái)并執(zhí)行resignFirstResponder()方法。
經(jīng)過(guò)試驗(yàn)發(fā)現(xiàn)我們可以通過(guò)如下方法修復(fù)這個(gè)問題
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 解決方案
let sel = Selector("firstResponder")
let fr = UIApplication.shared.keyWindow?.perform(sel)
if let view = fr?.takeRetainedValue() as? UIView {
view.resignFirstResponder()
}
if cell.isKind(of: WebCell.self) {
print("WebCell:willDisplay")
} else {
print("other:willDisplay")
}
}
在willDisplay代理中取出firstResponder,執(zhí)行resignFirstResponder()方法即可(實(shí)際開發(fā)需要考慮的問題這里暫不考慮)。
對(duì)應(yīng)的OC解決方法為
id first = [[UIApplication sharedApplication].keyWindow performSelector:@selector(firstResponder)];
if (first) {
[(UIView *)first resignFirstResponder];
}
最后附上demo鏈接: demo