如何检测UITextView完成内容重排以更新模糊视图?
我之前实现了一个BlurringView自定义UIView子类,通过blur布尔属性控制是否显示子视图的模糊版本,大部分场景下运行正常,但当子视图为UITextView时,设备旋转会出现问题:UITextView的bounds先随视图尺寸变化,但内容重排要等到旋转动画完成后才会执行。现有的尺寸监听方式(bounds的didSet、traitCollectionDidChange()、设备旋转通知)都会在内容重排前触发,直接更新模糊视图无法得到正确的效果,目前只能通过延迟0.33秒的“魔法数字”方案临时解决,代码如下:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { print("In \(#function))") // 仅当添加0.33秒延迟时,才能在TextView布局完成后更新模糊图 DispatchQueue.main.asyncAfter(deadline: .now() + 0.33) { print("Updating blur image. Bounds = \(self.bounds)") self.updateBlurImage() } }
以下是几种更优雅的替代方案,无需依赖固定延迟:
方案1:利用CATransaction监听旋转动画完成
设备旋转的尺寸变化属于系统动画,被包裹在CATransaction中。我们可以通过设置CATransaction的完成回调,在旋转动画结束后再更新模糊视图,此时UITextView的内容重排已完成:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) CATransaction.begin() CATransaction.setCompletionBlock { [weak self] in guard let self = self else { return } self.updateBlurImage() } CATransaction.commit() }
该方案无需针对UITextView做特殊判断,同时能覆盖所有系统驱动的视图尺寸变化场景。
方案2:KVO监听UITextView的contentSize变化
UITextView完成内容重排后,其contentSize会发生改变(尤其是宽度变化导致文本换行、高度调整时)。我们可以在BlurringView中对其子视图做类型判断,若为UITextView则添加KVO监听:
// 在BlurringView添加子视图时调用 func setupTextViewObserver() { guard let textView = subview as? UITextView else { return } textView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == "contentSize", object is UITextView else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } // 异步执行确保布局完全稳定 DispatchQueue.main.async { [weak self] in self?.updateBlurImage() } } // 记得在deinit中移除监听 deinit { if let textView = subview as? UITextView { textView.removeObserver(self, forKeyPath: "contentSize") } }
这种方式能精准捕捉UITextView的内容重排完成时机,即使是旋转以外的尺寸变化场景(比如父视图手动调整大小)也能生效。
方案3:自定义UITextView子类监听布局完成
创建UITextView的子类,在其layoutSubviews方法完成后触发回调,让BlurringView在回调中更新模糊视图:
class LayoutAwareTextView: UITextView { var onLayoutCompleted: (() -> Void)? override func layoutSubviews() { super.layoutSubviews() // 布局完成后通知外部 onLayoutCompleted?() } }
在BlurringView中使用该子类并绑定回调:
// 假设subview是LayoutAwareTextView实例 if let textView = subview as? LayoutAwareTextView { textView.onLayoutCompleted = { [weak self] in self?.updateBlurImage() } }
该方案逻辑直观,适合需要对UITextView布局做更多自定义处理的场景。
内容的提问来源于stack exchange,提问作者Duncan C

