WebView у SwiftUI для iOS 26: повний посібник з WebPage, JavaScript та навігацією

Нативний WebView у SwiftUI для iOS 26 — від базового завантаження URL до міні-браузера з WebPage, JavaScript, навігацією та фільтрацією доменів. Робочі приклади коду.

Вступ

Якщо ви хоч раз намагалися показати веб-контент у SwiftUI-додатку до iOS 26, то знаєте цей біль. Створити обгортку UIViewRepresentable навколо WKWebView, налаштувати делегати, побороти ретейн-цикли, якось синхронізувати імперативний UIKit із декларативним SwiftUI... Працювало? Так. Було зручно? Навряд.

На WWDC 2025 Apple нарешті вирішила цю проблему — і, чесно кажучи, краще пізно, ніж ніколи. Тепер у SwiftUI є нативний WebView, і відображення веб-сторінки зводиться до одного рядка коду. А для повного контролю — прогрес завантаження, JavaScript, навігація — є модель WebPage, побудована на протоколі Observable.

У цьому посібнику ми пройдемо весь шлях від найпростішого завантаження URL до побудови повноцінного браузера з історією навігації, фільтрацією доменів і виконанням JavaScript. Усі приклади — робочий код, який можна використовувати у своїх проєктах прямо зараз.

Базове завантаження URL: один рядок коду

Отже, найпростіший варіант. Хочете відобразити веб-сторінку? Просто передайте URL у WebView:

import SwiftUI
import WebKit

struct SimpleWebView: View {
    var body: some View {
        WebView(url: URL(string: "https://www.apple.com"))
    }
}

Зверніть увагу: WebView(url:) приймає опціональний URL?. Якщо URL некоректний або nil — нічого не завантажиться, і жодних крешів. Для роботи потрібно імпортувати фреймворк WebKit.

Такий підхід ідеально підходить для простих речей: показати сторінку політики конфіденційності, документацію або статичний контент. Компонент сам обробляє завантаження, підтримує жести навігації і добре інтегрується зі SwiftUI.

WebPage: повний контроль над веб-контентом

Якщо простого відображення недостатньо — скажімо, вам потрібно відстежувати прогрес завантаження, читати заголовок сторінки чи керувати навігацією програмно — тоді на сцену виходить WebPage. Це Observable-клас, створений спеціально для SwiftUI:

import SwiftUI
import WebKit

struct AdvancedWebView: View {
    @State private var page = WebPage()

    var body: some View {
        VStack(spacing: 0) {
            if page.isLoading {
                ProgressView(value: page.estimatedProgress)
                    .progressViewStyle(.linear)
            }

            WebView(page)
                .ignoresSafeArea(.container, edges: .bottom)
        }
        .navigationTitle(page.title ?? "Завантаження...")
        .onAppear {
            let request = URLRequest(url: URL(string: "https://www.swift.org")!)
            page.load(request)
        }
    }
}

Ключовий момент: WebPage.load() приймає URLRequestнеопціональним URL), а не просто URL-адресу. Це дає змогу додавати HTTP-заголовки, змінювати метод запиту й інші параметри — що набагато гнучкіше.

Властивості WebPage, які оновлюються автоматично

Оскільки WebPage відповідає протоколу Observable, будь-яка зміна його властивостей автоматично перемальовує залежні View. Ось основні:

  • page.title — заголовок поточної сторінки (String?)
  • page.url — поточний URL (URL?)
  • page.isLoading — чи завантажується сторінка (Bool)
  • page.estimatedProgress — прогрес завантаження від 0.0 до 1.0 (Double)

Це, м'яко кажучи, набагато простіше, ніж підписуватись на KVO-спостерігачі в WKWebView. Хто працював із KVO у Swift — той зрозуміє.

Конфігурація WebPage: User Agent, медіа та інше

Перед створенням WebPage можна налаштувати конфігурацію через WebPage.Configuration:

@State private var page: WebPage = {
    var config = WebPage.Configuration()
    config.applicationNameForUserAgent = "MyApp/1.0"
    return WebPage(configuration: config)
}()

Конфігурація дає змогу задати ідентифікацію додатку через User Agent, налаштувати поведінку медіа-відтворення, розпізнавання типів даних (посилання, номери телефонів тощо). Важливий нюанс: конфігурацію потрібно задати до створення WebPage. Змінити її після ініціалізації вже не вийде.

Завантаження локального HTML-контенту

WebPage підтримує не лише віддалені URL, а й завантаження HTML-рядків напряму. Це зручно для відображення стилізованого контенту, вбудованих відео або динамічно згенерованого HTML:

struct LocalHTMLView: View {
    @State private var page = WebPage()

    private let htmlContent = """
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            body {
                font-family: -apple-system, system-ui;
                padding: 20px;
                background-color: #f5f5f7;
                color: #1d1d1f;
            }
            h1 { color: #0066cc; }
        </style>
    </head>
    <body>
        <h1>Привіт із SwiftUI!</h1>
        <p>Цей HTML завантажено через WebPage.</p>
    </body>
    </html>
    """

    var body: some View {
        WebView(page)
            .onAppear {
                page.load(
                    html: htmlContent,
                    baseURL: URL(string: "about:blank")!
                )
            }
    }
}

Метод page.load(html:baseURL:) приймає HTML-рядок і базовий URL для розв'язання відносних шляхів. Якщо ваш HTML не посилається на зовнішні ресурси — about:blank цілком підійде.

Виконання JavaScript

Ось де стає по-справжньому цікаво. WebPage дозволяє виконувати довільний JavaScript на завантаженій сторінці. Метод callJavaScript — асинхронний і може повертати результат:

struct JavaScriptExample: View {
    @State private var page = WebPage()
    @State private var pageHeaders: [String] = []

    var body: some View {
        VStack {
            WebView(page)
                .ignoresSafeArea(.container, edges: .bottom)

            if !pageHeaders.isEmpty {
                List(pageHeaders, id: \.self) { header in
                    Text(header)
                }
                .frame(height: 200)
            }
        }
        .onAppear {
            let request = URLRequest(url: URL(string: "https://www.swift.org")!)
            page.load(request)
        }
        .task {
            // Чекаємо, поки сторінка завантажиться
            while page.isLoading {
                try? await Task.sleep(for: .milliseconds(100))
            }

            // Збираємо всі заголовки h2 зі сторінки
            do {
                let script = """
                    Array.from(document.querySelectorAll('h2'))
                        .map(el => el.textContent)
                        .join('|||')
                """
                if let result = try await page.callJavaScript(script) as? String {
                    pageHeaders = result.split(separator: "|||").map(String.init)
                }
            } catch {
                print("Помилка JavaScript: \(error)")
            }
        }
    }
}

callJavaScript(_:) працює через async/await, що ідеально вписується в модель конкурентності Swift. Скрипти виконуються в пісочниці WebKit, тож безпека забезпечена.

Важливо: завжди виконуйте JavaScript після завантаження сторінки. Якщо DOM ще не готовий — результат буде непередбачуваним. І ще: уникайте виконання ненадійного JavaScript-коду, це може створити вразливості у вашому додатку.

Навігація: кнопки «Назад» і «Вперед»

На відміну від Safari, WebView у SwiftUI не має вбудованих кнопок навігації. Їх потрібно зробити самостійно. Але не хвилюйтесь — WebPage надає всю необхідну інфраструктуру через властивість backForwardList:

struct BrowserView: View {
    @State private var page = WebPage()

    var body: some View {
        VStack(spacing: 0) {
            if page.isLoading {
                ProgressView(value: page.estimatedProgress)
                    .progressViewStyle(.linear)
            }

            WebView(page)
                .ignoresSafeArea(.container, edges: .bottom)
        }
        .onAppear {
            let request = URLRequest(url: URL(string: "https://www.swift.org")!)
            page.load(request)
        }
        .toolbar {
            ToolbarItemGroup(placement: .bottomBar) {
                Button {
                    if let item = page.backForwardList.backList.last {
                        page.load(item)
                    }
                } label: {
                    Label("Назад", systemImage: "chevron.backward")
                }
                .disabled(page.backForwardList.backList.isEmpty)

                Button {
                    if let item = page.backForwardList.forwardList.first {
                        page.load(item)
                    }
                } label: {
                    Label("Вперед", systemImage: "chevron.forward")
                }
                .disabled(page.backForwardList.forwardList.isEmpty)

                Spacer()

                Button {
                    page.reload()
                } label: {
                    Label("Оновити", systemImage: "arrow.clockwise")
                }

                if let url = page.url {
                    ShareLink(item: url)
                }
            }
        }
    }
}

Властивість backForwardList містить два масиви: backList (відвідані сторінки) та forwardList (сторінки для переходу вперед після натискання «Назад»). Кожен елемент — це WebPage.BackForwardList.Item з властивостями title і url.

Розширене меню історії

Для кращого UX можна додати меню з повною історією навігації — приблизно так, як Safari показує список сторінок при довгому натисканні на кнопку «Назад»:

struct BackForwardMenu: View {
    let list: [WebPage.BackForwardList.Item]
    let systemImage: String
    let navigateToItem: (WebPage.BackForwardList.Item) -> Void

    var body: some View {
        Menu {
            ForEach(list) { item in
                Button(item.title ?? item.url.absoluteString) {
                    navigateToItem(item)
                }
            }
        } label: {
            Label("Навігація", systemImage: systemImage)
        } primaryAction: {
            if let first = list.first {
                navigateToItem(first)
            }
        }
        .disabled(list.isEmpty)
    }
}

Одне натискання — перехід на попередню або наступну сторінку. Довге натискання — повний список історії. Зручно і звично для користувачів.

Контроль навігації: фільтрація URL та протокол NavigationDeciding

У реальних додатках часто потрібно контролювати, куди може переходити користувач. Наприклад, обмежити навігацію вашим доменом або перенаправити зовнішні посилання в системний браузер. Саме для цього WebPage надає протокол NavigationDeciding:

final class DomainNavigationDecider: WebPage.NavigationDeciding {
    var onExternalURL: ((URL) -> Void)?

    private let allowedDomain: String

    init(allowedDomain: String) {
        self.allowedDomain = allowedDomain
    }

    func decidePolicy(
        for action: WebPage.NavigationAction,
        preferences: inout WebPage.NavigationPreferences
    ) async -> WKNavigationActionPolicy {
        guard let url = action.request.url else { return .cancel }

        if url.host() == allowedDomain {
            return .allow
        }

        onExternalURL?(url)
        return .cancel
    }
}

А ось як це використовувати у View:

struct FilteredWebView: View {
    @Environment(\.openURL) private var openURL
    @State private var page = WebPage()

    var body: some View {
        WebView(page)
            .onAppear {
                let decider = DomainNavigationDecider(
                    allowedDomain: "www.swift.org"
                )
                decider.onExternalURL = { url in
                    openURL(url)
                }
                page = WebPage(navigationDecider: decider)
                page.load(URLRequest(url: URL(string: "https://www.swift.org")!))
            }
    }
}

Тепер якщо користувач натисне на зовнішнє посилання (наприклад, на GitHub), навігація всередині WebView скасується, а URL відкриється в Safari. Класичний патерн для додатків, що показують власний контент у вбудованому браузері.

Модифікатори WebView: жести та прев'ю посилань

SwiftUI надає кілька модифікаторів для тонкого налаштування WebView:

WebView(page)
    // Вимкнути прев'ю посилань при довгому натисканні
    .webViewLinkPreviews(.disabled)
    // Вимкнути жести свайпу для навігації назад/вперед
    .webViewBackForwardNavigationGestures(.disabled)

За замовчуванням обидві функції увімкнені. Прев'ю показує попередній перегляд сторінки при довгому натисканні, а жести свайпу дозволяють переходити назад і вперед горизонтальним рухом. Якщо ви вже реалізували навігацію через тулбар — жести свайпу краще вимкнути, щоб не було конфліктів.

Міграція з WKWebView: підтримка старіших версій iOS

Нативний WebView доступний тільки з iOS 26. Якщо ваш додаток має підтримувати старіші версії — використовуйте умовну компіляцію:

struct CrossVersionWebView: View {
    let url: URL

    var body: some View {
        if #available(iOS 26, *) {
            WebView(url: url)
        } else {
            LegacyWebView(url: url)
        }
    }
}

// Обгортка для старих версій iOS
struct LegacyWebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.load(URLRequest(url: url))
    }
}

Такий підхід забезпечує плавну міграцію: на iOS 26 і новіше працює нативний компонент, на старших версіях — перевірена обгортка UIViewRepresentable. Нічого складного.

Порівняння WKWebView та нативного WebView

Ось ключові відмінності між старим і новим підходами:

  • Налаштування: WKWebView вимагає UIViewRepresentable плюс делегати; WebView — один рядок коду.
  • Синтаксис: WKWebView — імперативний UIKit; WebView — декларативний SwiftUI.
  • Стан: WKWebView потребує ручної синхронізації через KVO; WebPage автоматично оновлює UI завдяки Observable.
  • JavaScript: обидва підтримують, але WebPage використовує async/await замість completion handlers.
  • Пам'ять: WebView усуває проблеми з ретейн-циклами, типові для UIViewRepresentable-обгорток.

Загалом, якщо ви починаєте новий проєкт під iOS 26 — немає жодної причини тягнути за собою WKWebView.

Повний приклад: міні-браузер у SwiftUI

Ну що, зберемо все воєдино? Ось повноцінний міні-браузер з адресним рядком, прогресом завантаження, навігацією та обробкою помилок:

import SwiftUI
import WebKit

struct MiniBrowser: View {
    @State private var page = WebPage()
    @State private var urlText = "https://www.swift.org"
    @State private var showError = false

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Індикатор прогресу
                if page.isLoading {
                    ProgressView(value: page.estimatedProgress)
                        .progressViewStyle(.linear)
                        .tint(.blue)
                }

                // Веб-контент
                WebView(page)
                    .ignoresSafeArea(.container, edges: .bottom)
            }
            .navigationTitle(page.title ?? "Браузер")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                // Адресний рядок
                ToolbarItem(placement: .principal) {
                    HStack {
                        TextField("URL", text: $urlText)
                            .textFieldStyle(.roundedBorder)
                            .autocorrectionDisabled()
                            .textInputAutocapitalization(.never)
                            .onSubmit { loadURL() }

                        Button("Перейти") { loadURL() }
                            .buttonStyle(.bordered)
                    }
                }

                // Навігаційні кнопки
                ToolbarItemGroup(placement: .bottomBar) {
                    Button {
                        if let item = page.backForwardList.backList.last {
                            page.load(item)
                        }
                    } label: {
                        Image(systemName: "chevron.backward")
                    }
                    .disabled(page.backForwardList.backList.isEmpty)

                    Button {
                        if let item = page.backForwardList.forwardList.first {
                            page.load(item)
                        }
                    } label: {
                        Image(systemName: "chevron.forward")
                    }
                    .disabled(page.backForwardList.forwardList.isEmpty)

                    Spacer()

                    if page.isLoading {
                        Button {
                            page.stopLoading()
                        } label: {
                            Image(systemName: "xmark")
                        }
                    } else {
                        Button {
                            page.reload()
                        } label: {
                            Image(systemName: "arrow.clockwise")
                        }
                    }

                    if let url = page.url {
                        ShareLink(item: url)
                    }
                }
            }
            .onAppear { loadURL() }
            .alert("Некоректний URL", isPresented: $showError) {
                Button("OK") {}
            }
        }
    }

    private func loadURL() {
        var input = urlText.trimmingCharacters(in: .whitespacesAndNewlines)
        if !input.hasPrefix("http://") && !input.hasPrefix("https://") {
            input = "https://" + input
        }
        guard let url = URL(string: input) else {
            showError = true
            return
        }
        page.load(URLRequest(url: url))
    }
}

Цей приклад покриває більшість можливостей нативного WebView: завантаження сторінок, прогрес, навігацію назад/вперед, перезавантаження, зупинку завантаження і шерінг URL. І все це — без жодного рядка UIKit-коду. Особисто мене це неймовірно тішить після років боротьби з UIViewRepresentable.

Поради та найкращі практики

  • Завжди використовуйте HTTPS. Завантаження HTTP-контенту заблоковане за замовчуванням через App Transport Security. Якщо вам справді потрібен HTTP — додайте виняток в Info.plist, але це вкрай не рекомендується.
  • Перевіряйте готовність DOM перед JavaScript. Виконуйте скрипти тільки після завершення завантаження, інакше callJavaScript може повернути неочікуваний результат або помилку.
  • Обробляйте помилки JavaScript. Метод callJavaScript може кинути помилку — завжди загортайте виклик у do-catch.
  • Використовуйте .ignoresSafeArea для WebView, щоб контент заповнював весь екран до країв.
  • Конфігуруйте WebPage один раз. WebPage.Configuration задається при ініціалізації і змінити її пізніше неможливо.
  • Уникайте @StateObject. WebPage — це Observable-клас, який працює з @State, а не з @StateObject. Не переплутайте (це поширена помилка).

Часті запитання (FAQ)

Чи можна використовувати SwiftUI WebView на версіях iOS до 26?

Ні, нативний WebView у SwiftUI доступний тільки з iOS 26. Для старших версій використовуйте обгортку UIViewRepresentable навколо WKWebView з умовною перевіркою #available(iOS 26, *).

У чому різниця між WebView і WebPage у SwiftUI?

WebView — це SwiftUI View для відображення веб-контенту. WebPage — це Observable-модель, яка дає повний контроль: заголовок, URL, прогрес завантаження, JavaScript, навігація. Для простих випадків вистачить WebView(url:), а для складних — створіть WebPage і передайте його у WebView(page).

Як виконати JavaScript у SwiftUI WebView?

Використовуйте метод callJavaScript(_:) на екземплярі WebPage. Він асинхронний (async throws) і повертає результат виконання скрипту. Головне — переконайтесь, що сторінка вже повністю завантажена.

Чи замінює новий WebView WKWebView?

Не повністю. Нативний WebView побудований на тому ж WebKit, але не дає доступу до всіх можливостей WKWebView — наприклад, WKUIDelegate для JavaScript-діалогів або детального налаштування WKWebViewConfiguration. Для більшості задач нового WebView більш ніж достатньо, але для специфічних сценаріїв WKWebView все ще може знадобитись.

Як обмежити навігацію певним доменом у WebView?

Реалізуйте протокол WebPage.NavigationDeciding. У методі decidePolicy(for:preferences:) перевірте домен запиту і поверніть .allow або .cancel. Зовнішні URL можна перенаправити у Safari через openURL.

Про Автора Editorial Team

Our team of expert writers and editors.