首页 > 解决方案 > 连续两次快速设置 UITextView 的 .attributedText 时,第二次赋值无效

问题描述

我有一个非常简单的UIViewController子类,它在以下位置配置其视图viewDidLoad

class TextViewController: UIViewController {

    private var textView: UITextView?

    var htmlText: String? {
        didSet {
            updateTextView()
        }
    }

    private func updateTextView() {
        textView?.setHtmlText(htmlText)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        textView = UITextView()

        // add as subview, set constraints etc.

        updateTextView()
    }
}

.setHtmlText是 UITextView 上的一个扩展,它把 HTML 变成了一个NSAttributedString,受这个答案的启发)

创建一个 TextViewController 的实例,.htmlText设置为“Fetching...”,发出一个 HTTP 请求,并将 viewcontroller 推送到 UINavigationController。

这会导致调用updateTextView无效(.textView仍然为 nil),但viewDidLoad通过再次调用它来确保显示当前文本值。不久之后,HTTP 请求返回一个响应,并.htmlText设置为该响应的主体,从而导致另一个调用updateTextView.

所有这些代码都在主队列上运行(通过设置断点和检查堆栈跟踪来确认),但除非在 http get 中有明显延迟,否则显示的最终文本是占位符(“Fetching...” )。在调试器中单步执行显示该序列是:

1. updateTextView()     // htmlText = "Fetching...", textView == nil
2. updateTextView()     // htmlText = "Fetching...", textView == UITextView
3. updateTextView()     // htmlText = <HTTP response body>
4. setHtmlText(<HTTP response body>)
5. setHtmlText("Fetching...")

所以不知何故,最后一个电话setHtmlText似乎超过了第一个。同样奇怪的是,从 #5 回顾调用堆栈,虽然setHtmlText声称它已通过“Fetching...”,但调用者认为它正在传递 HTTP HTML 正文。

更改 HTTP 响应的接收者以执行此操作:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { vc.htmlText = html }

而不是更传统的:

DispatchQueue.main.async { vc.htmlText = html }

...确实会导致显示预期的最终文本。

所有这些行为都可以在模拟器或真实设备上重现。一种有点奇怪的感觉“解决方案”是再次调用updateTextViewin viewWillAppear,但这只是掩盖了正在发生的事情。

编辑添加:

updateTextView我确实想知道只调用一次in是否足够viewWillAppear,但需要从viewDidLoadAND调用它viewWillAppear才能显示最终值。

编辑添加请求的代码:

let theVc = TextViewController()

theVc.htmlText = "<i>Fetching...</i>"

service.get(from: url) { [weak theVc] (result: Result<String>) in

    // DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    DispatchQueue.main.async {
        switch result {
        case .success(let html):
            theVc?.htmlText = html
        case .error(let err):
            theVc?.htmlText = "Failed: \(err.localizedDescription)"
        }
    }
}

navigationController.pushViewController($0, animated: true)

编辑添加简化案例,消除 HTTP 服务,具有相同的行为:

let theVc = TextViewController()

theVc.htmlText = "<i>Before...</i>"

DispatchQueue.main.async {
    theVc.htmlText = "<b>After</b>"
}

navigationController.pushViewController(theVc, animated: true)

这会产生updateTextView()与以前相同的调用序列:

  1. “之前”,还没有 textView
  2. “前”
  3. “后”

然而,“之前”是我在屏幕上看到的。

在 ("Before")开始处设置断点setHtmlText并逐步执行表明,当第一次通过在NSAttributedString(data:options:documentAttributes:)运行循环中时,重新进入第二次分配 ("After") 有机会运行到完成,分配结果是.attributedText。然后,原始的 NSAttributedString 有机会完成并立即替换.attributedText.

NSAttributedString这是从 HTML 生成 s的方式的一个怪癖(请参阅填充 a 时遇到类似问题UITableView的人)

标签: swift

解决方案


我通过消除您的扩展并简单地编写设置文本视图的属性文本以使用串行调度队列的代码解决了这个问题。这是我的文本视图控制器:

@IBOutlet private var textView: UITextView?
let q = DispatchQueue(label:"textview")
var htmlText: String? {
    didSet {
        updateTextView()
    }
}
override func viewDidLoad() {
    super.viewDidLoad()
    updateTextView()
}
private func updateTextView() {
    guard self.isViewLoaded else {return}
    guard let s = self.self.htmlText else {return}
    let f = self.textView!.font!
    self.q.async {
        let modifiedFont = String(format:"<span style=\"font-family: '-apple-system', 'HelveticaNeue'; font-size: \(f.pointSize)\">%@</span>", s)
        DispatchQueue.main.async {
            let attrStr = try! NSAttributedString(
                data: modifiedFont.data(using: .unicode, allowLossyConversion: true)!,
                options: [.documentType: NSAttributedString.DocumentType.html],
                documentAttributes: nil)
            self.textView!.attributedText = attrStr
        }
    }
}

添加print语句表明一切都按照预期的顺序(htmlText设置的顺序)发生。


推荐阅读