Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Интерфейс приложения
Мы уже добавили в проект (код доступен на GitHub – прим. ред.) перечисление WebViewNavigationAction, которое описывает три действия: назад, вперед, перезагрузить. Создадим для них SwiftUI View, и назовем ее WebNavigationView, в который добавим кнопки действий. Поскольку WebView подгружает веб-страницу из интернета, а это действие не происходит мгновенно, добавим 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()
}
}
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 оставим пустыми. Вернемся к ним позже.
ViewModel
Мы добрались до самого интересного! Теперь подружим все элементы вместе. Для начала создадим ViewModel и подумаем, чего мы хотим от WebView.
- Получить заголовок веб-страницы – добавим свойство WebTitle;
- Определять действия навигации – добавим свойство webViewNavigationPublisher;
- Определять когда нужно показать LoaderView – добавим свойство isLoaderVisible.
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; или зажмите ⌃⌘ и кликните). Как видите, все методы опциональные и не требуют реализации.
Далее приведем описание функций, чтобы вы их знали и могли использовать в своих целях.
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!)
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!)
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
func webViewWebContentProcessDidTerminate(_ webView: WKWebView)
func webView(_ webView: WKWebView, authenticationChallenge challenge: URLAuthenticationChallenge, shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void)
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
}
Готово! Давайте протестируем, как это работает.

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