我有一个非常简单的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
进行一次调用是否足够,但是需要从viewDidLoad
和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()
相同的调用序列:
但我在屏幕上看到的是“之前”。
在setHtmlText
(“ Before”)的开头设置一个断点并逐步执行,可以发现,当第一遍位于NSAttributedString(data:options:documentAttributes:)
时,重新进入运行循环,第二次分配(有机会给“之后”)运行完成的机会,并将其结果分配给.attributedText
。然后,原始NSAttributedString有机会完成,并立即替换.attributedText
。
这是从HTML生成NSAttributedString
的方式的怪癖(请参见在填充UITableView
时有similar issues的人)
答案 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()