Интерфейс приложения
Мы уже добавили в проект (код доступен на GitHub – прим. ред.) перечисление WebViewNavigationAction , которое описывает три действия: назад, вперед, перезагрузить. Создадим для них SwiftUI View , и назовем ее WebNavigationView , в который добавим кнопки действий. Поскольку WebView подгружает веб-страницу из интернета, а это действие не происходит мгновенно, добавим LoaderView чтобы показать пользователю прогресс загрузки.
На создании пользовательского интерфейса подробно останавливаться не будем: просто покажем его реализацию. Пишите в комментариях, какие темы вам хотелось бы увидеть в следующих статьях. LoaderView
import SwiftUI struct LoaderView: View { @State var isSpinCircle = false var body: some View { ZStack { Circle() .frame(width: 60, height: 60, alignment: .center) VStack { Circle() .trim(from: 0.3, to: 1) .stroke(Color.white, lineWidth: 2) .frame(width:50, height: 50) .padding(.all, 8) .rotationEffect(.degrees(isSpinCircle ? 0 : -360), anchor: .center) .animation(Animation.linear(duration: 0.6).repeatForever(autoreverses: false)) .onAppear { self.isSpinCircle = true } } } } } struct LoaderView_Previews: PreviewProvider { static var previews: some View { LoaderView() } }
WebNavigationView
import SwiftUI struct WebNavigationView: View { var body: some View { VStack { Divider() HStack(spacing: 10) { Divider() Button(action: {}, label: { Image(systemName: "chevron.left") .font(.system(size: 30, weight: .regular)) .imageScale(.medium) }) Divider() Button(action: {}, label: { Image(systemName: "chevron.right") .font(.system(size: 30, weight: .regular)) .imageScale(.medium) }) Divider() Spacer() Divider() Button(action: {}, label: { Image(systemName: "arrow.clockwise") .font(.system(size: 30, weight: .regular)) .imageScale(.medium) }) Divider() } .frame(height: 50) Divider() } } } struct WebNavigationView_Previews: PreviewProvider { static var previews: some View { WebNavigationView() } }
Пока действие нажатия на кнопку action оставим пустыми. Вернемся к ним позже.
Пара слов о ObservableObject @ObservableObject – это обертка, которую мы можем разделить между несколькими View . View могут подписываться и наблюдать за изменениями этого объекта.
ViewModel
Мы добрались до самого интересного! Теперь подружим все элементы вместе. Для начала создадим ViewModel и подумаем, чего мы хотим от WebView .
Получить заголовок веб-страницы – добавим свойство WebTitle ;
Определять действия навигации – добавим свойство webViewNavigationPublisher ;
Определять когда нужно показать LoaderView – добавим свойство isLoaderVisible .
ViewModel
import Foundation import Combine class ViewModel: ObservableObject { var isLoaderVisible = PassthroughSubject<Bool, Never>(); var webTitle = PassthroughSubject<String, Never>() var webViewNavigationPublisher = PassthroughSubject<WebViewNavigationAction, Never>() }
Обновим ContentView , определим ViewModel и состояния isLoaderVisible :
@ObservedObject var viewModel = ViewModel() @State var isLoaderVisible = false
Пора добавить весь созданный интерфейс в ContentView
var body: some View { ZStack { VStack(spacing: 0) { WebNavigationView() WebView(type: .public, url: "https://proglib.io", viewModel: viewModel) } if isLoaderVisible { LoaderView() } } }
Теперь вернемся в WebView и добавим свойство @ObservedObject var viewModel: ViewModel
, а также метод makeCoordinator , который будет возвращать Coordinator для взаимодействия с функциями делагата из WKNavigationDelegate .
func makeCoordinator() -> Coordinator { Coordinator(self) }
Напишем реализацию класса Coordinator .
class Coordinator: NSObject, WKNavigationDelegate { var parent: WebView var webViewNavigationSubscriber: AnyCancellable? = nil init(_ webView: WebView) { self.parent = webView } deinit { webViewNavigationSubscriber?.cancel() } }
Прежде чем заняться навигацией в WebView , проверим что все работает. Изменим состояние isLoaderVisible на true и посмотрим результат в preview .
Погружение в WKNavigationDelegate
Давайте посмотрим, что нам предлагает реализовать протокол. Перейдите к его определению (Jump to Definition в Xcode ; или зажмите ⌃⌘ и кликните). Как видите, все методы опциональные и не требуют реализации.
Далее приведем описание функций, чтобы вы их знали и могли использовать в своих целях.
*** didStartProvisionalNavigation Метод вызывается при запуске навигации по главному фрейму.
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)
didFailProvisionalNavigation Метод вызывается, когда происходит ошибка при запуске загрузки данных в главный фрейм.
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
didReceiveServerRedirectForProvisionalNavigation Метод вызывается, когда сервер получил перенаправление для главного фрейма.
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!)
didCommit Метод вызывается, когда начинает поступать контент для главного фрейма.
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!)
didFail Метод вызывается, когда возникает ошибка при комите (фиксации) в главном фрейме навигации.
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)
didFinish Метод вызывается, когда навигация завершена.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
didReceive Метод вызывается, когда WebView необходимо ответить на запрос аутентификации.
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
webViewWebContentProcessDidTerminate Метод вызывается, когда обработка контента в WebView прервана.
func webViewWebContentProcessDidTerminate(_ webView: WKWebView)
authenticationChallenge Метод вызывается, когда WebView устанавливает соединение с использованием устаревшей версии TLS.
func webView(_ webView: WKWebView, authenticationChallenge challenge: URLAuthenticationChallenge, shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void)
decidePolicyFor Метод принимает решение о разрешении или отклонении навигации на основе известного ответа. Здесь стоит отметить, что если не реализовать этот метод, то WebView загрузит запрос и при необходимости перенаправит в другое приложение.
Web Navigation
Добавим реализацию основных функций делегата в проект и пропишем везде вывод в консоль, для наглядности (пользователю нужно видеть, что происходит при загрузке).
В методе makeUIView , у WebView укажем координатор, где мы реализовали протокол WKNavigationDelegate
webView.navigationDelegate = context.coordinator
Поскольку мы добавили реализацию decidePolicyFor , нужно явно определить политику навигации. WKNavigationActionPolicy – это перечисление с двумя значениями allow и cancel . На данном этапе мы разрешим все.
decisionHandler(.allow, preferences)
Протестируйте проект и посмотрите, как работает приложение.
Займемся состоянием загрузки веб-страницы. Из описаний функций становится понятно, когда нужно скрыть или показать LoaderView – мы просто сообщим ViewModel необходимое состояние.
self.parent.viewModel.isLoaderVisible.send(true)
В ContentView обработаем это действие. Вызовем у VStack onReceive и установим состоянию isLoaderVisible значение из ViewModel
VStack(spacing: 0) { // views }.onReceive(self.viewModel.isLoaderVisible.receive(on: RunLoop.main)) { value in self.isLoaderVisible = value }
Отлично, процесс загрузки веб-страницы работает как часы.
Вернемся в WebView и добавим логику для действий навигации в функции didStartProvisionalNavigation
self.webViewNavigationSubscriber = self.parent.viewModel.webViewNavigationPublisher.receive(on: RunLoop.main).sink(receiveValue: { navigation in switch navigation { case .backward: if webView.canGoBack { webView.goBack() } case .forward: if webView.canGoForward { webView.goForward() } case .reload: webView.reload() } })
Осталось добавить обработчики действий в WebNavigationView , которые мы оставили пустыми. Необходимо передать ViewModel во View и отправить соответствующее действие для каждой кнопки. Например, для перезагрузки:
viewModel.webViewNavigationPublisher.send(.reload)
Прежде чем тестировать навигацию, поработаем над получением title веб-страницы.
При помощи метода evaluateJavaScript из webView мы можем вызвать любой код на JavaScript и получать результат в Swift, т.е. получить любую информацию с веб-страницы. В методе didFinish , когда навигация завершена, получим title и сообщим его ViewModel .
webView.evaluateJavaScript("document.title") { (response, error) in if let error = error { print("Error evaluateJavaScript") print(error.localizedDescription) } guard let title = response as? String else { return } self.parent.viewModel.showWebTitle.send(title) }
Теперь покажем его в WebNavigationView.
Для этого добавим состояние @State var webTitle = ""
, значение которого будем показывать в Text (разместим его между Divider и Spacer ).
Text(webTitle).onReceive(self.viewModel.webTitle.receive(on: RunLoop.main)) { value in self.webTitle = value }
Готово! Давайте протестируем, как это работает.
Первая часть цикла доступна по ссылке . Продолжение следует…