快速连续两次设置UITextView的.attributedText时,第二个分配无效

时间:2019-03-28 10:27:53

标签: swift

我有一个非常简单的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的扩展,它受this answer的启发将HTML变成了NSAttributedString

已创建TextViewController的实例,将.htmlText设置为“正在获取...”,发出了HTTP请求,并将视图控制器推到了UINavigationController上。

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

所有这些代码都在主队列上运行(通过设置断点并检查堆栈跟踪来确认),但是,除非http获取有明显的延迟,否则显示的最终文本将是占位符(“获取。 ..”)。逐步进入调试器后,发现序列为:

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 }

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

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

编辑后添加:

我确实想知道仅在updateTextView中对viewWillAppear进行一次调用是否足够,但是需要从viewDidLoadviewWillAppear进行调用以获取最终值显示。

已编辑以添加请求的代码:

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. “之后”

但我在屏幕上看到的是“之前”。

setHtmlText(“ Before”)的开头设置一个断点并逐步执行,可以发现,当第一遍位于NSAttributedString(data:options:documentAttributes:)时,重新进入运行循环,第二次分配(有机会给“之后”)运行完成的机会,并将其结果分配给.attributedText。然后,原始NSAttributedString有机会完成,并立即替换.attributedText

这是从HTML生成NSAttributedString的方式的怪癖(请参见在填充UITableView时有similar issues的人)

2 个答案:

答案 0 :(得分:1)

我解决了这一问题,消除了您的扩展名,只需编写代码即可设置文本视图的属性文本以使用串行调度队列。这是我的TextViewController:

@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的顺序)进行。

答案 1 :(得分:0)

如何解决问题?

  private var textView: UITextView? = UITextView()

在ViewDidLoad()中删除updateTextView()textView = UITextView()