Share This
Связаться со мной
Крути в низ
Categories
//Взаимодействие SwiftUI с вебом. Часть вторая: Web Navigation

Взаимодействие SwiftUI с вебом. Часть вторая: Web Navigation

В предыдущей статье мы создали WebView и подгрузили в него сайт proglib.io. Сегодня займемся пользовательским интерфейсом приложения, навигацией и получением информации с веб-страницы в Swift c помощью JavaScript. Обсудить

vzaimodejstvie swiftui s vebom chast vtoraja web navigation d771f41 - Взаимодействие SwiftUI с вебом. Часть вторая: Web Navigation

Интерфейс приложения

Мы уже добавили в проект (код доступен на 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.

  1. Получить заголовок веб-страницы – добавим свойство WebTitle;
  2. Определять действия навигации – добавим свойство webViewNavigationPublisher;
  3. Определять когда нужно показать 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.

vzaimodejstvie swiftui s vebom chast vtoraja web navigation 9cc49ee - Взаимодействие SwiftUI с вебом. Часть вторая: Web Navigation

Погружение в 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 				}     

Готово! Давайте протестируем, как это работает.

vzaimodejstvie swiftui s vebom chast vtoraja web navigation 974d21b - Взаимодействие SwiftUI с вебом. Часть вторая: Web Navigation

Первая часть цикла доступна по ссылке. Продолжение следует…

  • 10 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close