前言
先说问题是什么,那就是当UICollectionView的cell上有webview且在webview中进行过点击操作后,webview所在cell就无法触发UICollectionView的代理方法willDisplay
和didEndDisplaying
。
demo链接: demo
如何发现
在做分页支持web活动页的需求时,QA找了一个全是视频的链接测试。我们的webview封装VC在willAppear
和willDisappear
方法中都会告诉前端进入或者离开页面从而达到离开页面暂停播放的效果。而分页的框架是由UICollectionView实现的,其中每个cell的willAppear
和willDisappear
方法都是由UICollectionView的willDisplay
和didEndDisplaying
代理方法来实现的。一旦在cell中的webview播放完成视频,点击重播后,这个cell就再也不会触发上述这两个方法了,进而导致web中的视频没法暂停。
在经过排查后发现这个问题与视频无关,只要在webview中进行过点击操作后就会出现。自测发现在iOS13上无论是UIWebView还是WKWebView,都存在这个问题。QA也在iOS9上发现相同的问题。
复现
我们使用两种cell来复现这个问题,一个是有带webview的cell,一种是普通的cell。我们分别在willDisplay
和didEndDisplaying
代理方法中加入如下代码
if cell.isKind(of: WebCell.self) {
print("WebCell:willDisplay")
} else {
print("other:willDisplay")
}
在CollectionView中我们在左右两侧的cell为普通的cell,中间的cell为携带webview的cell,如图1。
在三个cell中切换我们会输出如下信息
other:willDisplay
WebCell:willDisplay
other:didEndDisplaying
other:willDisplay
WebCell:didEndDisplaying
但一旦在webview中对按钮进行点击操作,就会输出如下信息
other:willDisplay
other:didEndDisplaying
other:willDisplay
other:didEndDisplaying
other:willDisplay
我们可以发现这些信息中少了webview的信息,也就意味着webcell走不了willDisplay
和didEndDisplaying
代理方法。
经过测试,如果webCell可以被复用,那么当多个webCell满足无法触发代理的条件并经过复用后,有且仅有一个webCell无法触发willDisplay
和didEndDisplaying
代理方法。
原因
使用Xcode查看内存图可以发现正常的持有情况是如图2的。
但是如果你在webview内进行了点击操作后,它的内存图就变成了图3。
我们可以发现,webCell转而被一个私有属性_firstResponderView
持有了。我们可以推测出原因就是这个webCell原本应该在UICollectionView中的数组里,这样才可以触发willDisplay
和didEndDisplaying
代理方法,但现在已经不再这个数组里了,也就无法触发这两个代理方法。
解决方案
其实我们看到webCell被私有属性_firstResponderView
持有后,可以想到把keyWindow的firstResponder
取出来并执行resignFirstResponder()
方法。
经过试验发现我们可以通过如下方法修复这个问题
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
,执行resignFirstResponder()
方法即可(实际开发需要考虑的问题这里暂不考虑)。
对应的OC解决方法为
id first = [[UIApplication sharedApplication].keyWindow performSelector:@selector(firstResponder)];
if (first) {
[(UIView *)first resignFirstResponder];
}
最后附上demo链接: demo