Давайте воспользуемся возможностями платформы 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.
Вот и все. Спасибо за прочтение.