При установке .attributedText UITextView дважды в быстрой последовательности, второе назначение не имеет никакого эффекта - PullRequest
0 голосов
/ 28 марта 2019

У меня есть очень простой подкласс 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 установлено значение «Извлечение ...», выполнен HTTP-запрос, и viewcontroller помещен в UINavigationController.

Это приводит к вызову updateTextView, который не имеет никакого эффекта (.textView по-прежнему равен нулю), но 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(), как и раньше:

  1. "До", пока нет текстового обзора
  2. "До"
  3. "После"

И все же «До» - это то, что я вижу на экране.

Установка точки останова в начале setHtmlText («До») и пошаговое выполнение показывает, что, пока первый проход находится в NSAttributedString(data:options:documentAttributes:), цикл выполнения повторно вводится, а второе назначение («После») дается шанс дойти до завершения, присваивая его результат .attributedText. Затем исходному NSAttributedString дается шанс завершиться, и он немедленно заменяет .attributedText.

Это странный способ NSAttributedString s генерируется из HTML (см. Кто-то, имеющий схожие проблемы при заполнении UITableView)

Ответы [ 2 ]

1 голос
/ 28 марта 2019

Я решил эту проблему, исключив ваше расширение и просто написав код, который устанавливает атрибут текста для текстового представления для использования очереди последовательной отправки.Вот мой 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).

0 голосов
/ 28 марта 2019

Как насчет этого способа решения проблемы?

  private var textView: UITextView? = UITextView()

удалить updateTextView() и textView = UITextView() в ViewDidLoad ()

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...