Давайте воспользуемся возможностями платформы Combine, чтобы обойти эту проблему.

Представленный на WWDC 2019, SwiftUI изменил способ создания пользовательских интерфейсов для наших приложений. Вещи, которые при использовании UIKit и Auto Layouts занимают много времени и стандартного кода, могут быть выполнены очень быстро с помощью SwiftUI.

Обновлять представления SwiftUI действительно легко благодаря структуре, управляемой состоянием. Подключение ProgressBar и WKWebView в SwiftUI кажется легкой прогулкой.

Но что происходит, когда вам нужно изменить состояние ProgressBar с WKWebView при загрузке веб-страницы? Это цель данной статьи.

Наша цель

  • Создайте приложение SwiftUI для iOS, состоящее из WKWebView и ProgressBar. Мы будем обновлять ProgressBar по мере загрузки веб-страницы в SwiftUI.
  • Поймите, как изменение состояния во время обновления представления приведет к неопределенному поведению и несоответствиям в представлении SwiftUI.
  • Использование возможностей платформы Combine с оболочкой свойств @Published и ObservableObject для устранения этой проблемы в нашем приложении.

Создайте SwiftUI ProgressBar

SwiftUI на данный момент не предоставляет готовую реализацию для ProgressBars. К счастью, мы можем использовать SwiftUI Rectangle shape и GeometryReader для создания нашего собственного ProgressBar, как показано ниже:

Вот краткий обзор SwiftUI Previews с ProgressBar в действии. Мы использовали ползунок SwiftUI для обновления значения progress:

Теперь это было быстро и легко. Наша следующая цель - создать SwiftUI WebView и интегрировать его с ProgressBar.

Создание SwiftUI WebView с помощью протокола UIViewRepresentable

Как и в случае с ProgressBars, SwiftUI не предоставляет встроенной реализации для отображения WKWebViews. К счастью, мы можем использовать протокол UIViewRepresentable и использовать возможности взаимодействия UIKit-SwiftUI для создания структуры оболочки для WKWebView.

Функция makeUIView запускается при создании экземпляра структуры выше. Впоследствии нам нужно будет использовать updateUIView для обновления представления.

Вы заметите свойство @Binding, определенное в приведенной выше структуре - progress. Он используется для обновления ProgressBar состояния из WKWebView процесса загрузки страницы, который мы будем отслеживать.

Класс Coordinator используется для обработки делегатов UIKit и передачи данных обратно в представление SwiftUI. Мы передаем свойство Binding progress, указанное выше:

Метод observe требует estimatedProgress свойства объекта WKWebView, и мы использовали для него синтаксис Swift 5.2 Keypath.

Аргумент .new гарантирует, что любой новый прогресс загрузки страницы запускает наблюдателя.

Логика довольно проста. Мы устанавливаем значение estimateProgress для свойства Binding, которое обновляет SwiftUI ProgressBar. Но строчка ниже - вопиющая проблема:

self.progress = webView.estimatedProgress

SwiftUI дает нам следующее сообщение во время выполнения:

Warning: Modifying state during view update, this will cause undefined behavior.

Как и ожидалось, когда вы запускаете приложение SwiftUI, описанное выше, вы получаете ProgressBar, который продолжает обновляться, как показано ниже:

Как видите, ProgressBar продолжает показывать случайные значения, а WKWebView продолжает перезагружаться. Мы изменяли свойство State во время загрузки представления, что привело к повторной визуализации представлений. Apple может исправить это неопределенное поведение в следующем выпуске, но до тех пор нам нужно исправить проблему.

Исправить неопределенное поведение с помощью свойства @Published

Проблема с приведенным выше кодом заключалась в том, что мы пытались изменить состояние представления из другого состояния во время обновления представления, что приводило к перезагрузке всего тела.

Вместо использования потока данных SwiftUI, управляемого состоянием, мы можем использовать оболочку свойств @Published для реактивного обновления представлений. SwiftUI теперь предоставляет встроенную поддержку оболочки свойств Combine Published.

Давайте создадим класс, соответствующий протоколу ObservableObject, чтобы публиковать объявления в SwiftUI:

Свойство progress будет использоваться для реактивного обновления ProgressBar, а link установит строковый URL в WKWebView loadRequest.

Свойства экземпляра WebViewModel, который мы объявили ранее, теперь можно изменить из класса Coordinator, чтобы соответствующим образом обновить представление SwiftUI. Как видите, мы используем Publisher для реактивного обновления представления вместо использования States и Binding, которые вызывают проблемы при изменении во время обновления представления.

Код для представления SwiftUI, содержащего SwiftUIWebView выше и SwiftUIProgressBar, отображается ниже:

В приведенном выше коде ObservedObject публикует двойное значение при изменении свойства progress. Но для ProgressBar требуется свойство Binding. Итак, чтобы преобразовать значение Double в Binding<Double>, мы используем привязки constant.

Приложение теперь работает как шарм:

Заключительные мысли

SwiftUI - это структура, управляемая состоянием, которая позволяет нам легко создавать декларативные пользовательские интерфейсы. Но иногда, когда вам нужно изменить состояния во время обновления представления, лучше использовать мощь инфраструктуры Combine, поскольку она предотвращает проблемы с рендерингом и перезагрузкой.

Вы можете найти полный исходный код в этом репозитории GitHub.

Вот и все. Спасибо за прочтение.