Konec éry UIViewRepresentable pro webový obsah
Ruku na srdce — pokud jste někdy potřebovali zobrazit webovou stránku uvnitř SwiftUI aplikace, víte, jaký to byl boj. Vytvořit UIViewRepresentable wrapper kolem WKWebView, řešit delegáty, aktualizace stavu, koordinátory… Bylo to zbytečně složité na něco tak základního.
V iOS 26 to Apple konečně vyřešil. Představil nativní WebView přímo ve SwiftUI — jednoduchý, deklarativní a plně integrovaný do ekosystému. Jeden řádek kódu a máte plnohodnotný webový prohlížeč ve vaší aplikaci. Vážně, jeden řádek.
Pojďme si projít všechno od základního použití až po pokročilé věci jako spouštění JavaScriptu, řízení navigace nebo vlastní URL schémata.
Požadavky a import
Nativní WebView vyžaduje iOS 26 (případně macOS 26 nebo visionOS 3) jako minimální cílovou platformu. Kromě SwiftUI je nutné importovat i framework WebKit:
import SwiftUI
import WebKit
Pokud vaše aplikace cílí na starší verze iOS, budete muset nadále používat WKWebView přes UIViewRepresentable. Nebojte, k tomu se vrátíme na konci článku.
Základní zobrazení webové stránky
Nejjednodušší způsob, jak zobrazit webovou stránku ve SwiftUI, je předat URL přímo do WebView:
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.swift.org"))
}
}
A to je celé. Plně funkční webový prohlížeč — se scrollováním, fungujícími odkazy, videem i JavaScriptem. Parametr url je navíc volitelný (URL?), takže žádný force unwrap.
Pro plné využití obrazovky stačí přidat .ignoresSafeArea():
struct FullScreenWebView: View {
var body: some View {
WebView(url: URL(string: "https://www.swift.org"))
.ignoresSafeArea()
}
}
WebPage: Kdy potřebujete víc kontroly
Jednoduchý WebView(url:) je fajn pro statické zobrazení, ale co když chcete sledovat průběh načítání, zjistit titul stránky, spouštět JavaScript nebo řídit navigaci? Přesně na to je třída WebPage.
WebPage je Observable třída, takže automaticky aktualizuje vaše SwiftUI views, když se změní její vlastnosti. Vytvoříte ji jako @State property a předáte do WebView:
struct WebPageDemoView: View {
@State private var page = WebPage()
var body: some View {
VStack {
// Zobrazení titulu a URL stránky
if let title = page.title {
Text(title)
.font(.headline)
}
if let url = page.url {
Text(url.absoluteString)
.font(.caption)
.foregroundStyle(.secondary)
}
WebView(page)
.ignoresSafeArea()
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
Klíčové vlastnosti WebPage
Protože WebPage je Observable, všechny tyto vlastnosti automaticky spouštějí překreslení:
page.title— aktuální titul stránky (String?)page.url— aktuální URL (URL?)page.estimatedProgress— průběh načítání od 0.0 do 1.0 (Double)page.isLoading— zda se stránka právě načítá (Bool)page.backForwardList— historie navigace pro zpětnou/dopřednou navigaci
Sledování průběhu načítání
Asi nejčastější požadavek — ukázat uživateli, jak daleko je načítání. S WebPage je to opravdu triviální:
struct ProgressWebView: 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()
}
.onAppear {
if let url = URL(string: "https://www.apple.com") {
page.load(URLRequest(url: url))
}
}
}
}
Vlastnost estimatedProgress se plynule aktualizuje během načítání, takže ProgressView reaguje v reálném čase. Jakmile stránka doběhne, isLoading se přepne na false a progress bar prostě zmizí. Žádný ruční state management.
Navigační ovládání: Zpět, vpřed, obnovit
Přidání navigačních tlačítek je díky WebPage celkem přímočaré:
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()
HStack {
Button("Zpět") {
if let item = page.backForwardList.backItem {
page.load(item)
}
}
.disabled(page.backForwardList.backItem == nil)
Button("Vpřed") {
if let item = page.backForwardList.forwardItem {
page.load(item)
}
}
.disabled(page.backForwardList.forwardItem == nil)
Spacer()
Button("Obnovit") {
page.reload()
}
Button("Stop") {
page.stopLoading()
}
.disabled(!page.isLoading)
}
.padding()
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
Navigace zpět a vpřed používá backForwardList — stejný koncept jako u klasického WKWebView, ale plně integrovaný do reaktivního modelu SwiftUI. Pokud jste někdy psali delegáta jen proto, abyste zjistili, jestli je tlačítko Zpět aktivní, tohle vás potěší.
View modifikátory pro WebView
SwiftUI WebView přichází s vlastní sadou modifikátorů pro přizpůsobení chování:
WebView(page)
.webViewBackForwardNavigationGestures(.enabled)
.webViewMagnificationGestures(.enabled)
.webViewLinkPreviews(.disabled)
.webViewContentBackground(.hidden)
Tady je přehled toho, co máte k dispozici:
| Modifikátor | Popis |
|---|---|
webViewBackForwardNavigationGestures | Povolí/zakáže gesta pro přejetí zpět/vpřed |
webViewMagnificationGestures | Povolí/zakáže gesto pinch-to-zoom |
webViewLinkPreviews | Povolí/zakáže náhledy odkazů při dlouhém stisku |
webViewContentBackground | Skryje pozadí stránky pro vlastní SwiftUI pozadí |
webViewScrollPosition | Napojí scroll pozici na ScrollPosition binding |
webViewOnScrollGeometryChange | Reaguje na změny scroll geometrie |
findNavigator | Zobrazí/skryje rozhraní pro hledání na stránce |
Transparentní pozadí
Modifikátor webViewContentBackground(.hidden) se hodí, když chcete za webový obsah umístit vlastní SwiftUI pozadí. Třeba gradient:
WebView(page)
.webViewContentBackground(.hidden)
.background(
LinearGradient(
colors: [.blue, .purple],
startPoint: .top,
endPoint: .bottom
)
)
Hledání na stránce
Implementace funkce „Najít na stránce" je překvapivě jednoduchá — osobně jsem čekal, že to bude složitější:
struct SearchableWebView: View {
@State private var page = WebPage()
@State private var showFind = false
var body: some View {
WebView(page)
.findNavigator(isPresented: $showFind)
.toolbar {
Button("Hledat") {
showFind.toggle()
}
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
Spouštění JavaScriptu
Tady to začíná být opravdu zajímavé. Komunikace s webovým obsahem přes JavaScript je jedna z nejsilnějších stránek nového WebView. Metoda callJavaScript na WebPage podporuje async/throws:
struct JavaScriptDemoView: View {
@State private var page = WebPage()
@State private var pageTitle = ""
var body: some View {
VStack {
Text("Titul stránky: \(pageTitle)")
.font(.headline)
WebView(page)
.ignoresSafeArea()
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
.task {
// Počkáme, až se stránka načte
while page.isLoading {
try? await Task.sleep(for: .milliseconds(100))
}
// Spustíme JavaScript
do {
let result = try await page.callJavaScript("document.title")
if let title = result as? String {
pageTitle = title
}
} catch {
print("Chyba JS: \(error)")
}
}
}
}
Pokročilý příklad: Extrakce obsahu stránky
Představte si, že chcete z načtené stránky vytáhnout všechny nadpisy a vytvořit z nich navigační menu. S callJavaScript a pojmenovanými argumenty to jde elegantně:
struct TableOfContentsView: View {
@State private var page = WebPage()
@State private var headers: [(id: String, title: String)] = []
var body: some View {
HStack {
// Sidebar s obsahem
List(headers, id: \.id) { header in
Button(header.title) {
Task {
try? await page.callJavaScript(
"document.getElementById(arguments.sectionId)?.scrollIntoView({behavior: 'smooth'})",
arguments: ["sectionId": header.id]
)
}
}
}
.frame(width: 250)
// Webový obsah
WebView(page)
}
.task {
if let url = URL(string: "https://www.swift.org/documentation/") {
page.load(URLRequest(url: url))
}
while page.isLoading {
try? await Task.sleep(for: .milliseconds(100))
}
await extractHeaders()
}
}
private func extractHeaders() async {
let js = """
const headers = document.querySelectorAll("h2, h3")
return [...headers].map(h => ({
id: h.id || h.textContent.replaceAll(" ", "-").toLowerCase(),
title: h.textContent
}))
"""
do {
let result = try await page.callJavaScript(js)
if let items = result as? [[String: Any]] {
headers = items.compactMap { item in
guard let id = item["id"] as? String,
let title = item["title"] as? String else { return nil }
return (id: id, title: title)
}
}
} catch {
print("Chyba při extrakci nadpisů: \(error)")
}
}
}
Za zmínku stojí, že callJavaScript přijímá pojmenované argumenty přes parametr arguments — v JavaScriptu jsou pak dostupné přes objekt arguments. Výsledek je typu Any?, takže ho musíte přetypovat.
Načítání lokálního HTML
Kromě vzdálených URL můžete načíst i lokální HTML — ať už jako řetězec nebo ze souboru v bundle:
struct LocalHTMLView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
let html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, system-ui;
padding: 20px;
color: #333;
}
h1 { color: #007AFF; }
</style>
</head>
<body>
<h1>Ahoj ze SwiftUI!</h1>
<p>Toto je lokální HTML obsah načtený přes WebPage.</p>
</body>
</html>
"""
page.load(
html: html,
baseURL: Bundle.main.resourceURL!
)
}
}
}
Použití Bundle.main.resourceURL! jako baseURL umožňuje HTML odkazovat na soubory v bundle — styly CSS, obrázky, skripty. Šikovné, pokud stavíte hybridní aplikaci s částí UI v HTML.
Řízení navigace s NavigationDeciding
Protokol WebPage.NavigationDeciding vám dává plnou kontrolu nad tím, kam se uživatel může z vašeho WebView dostat. Klasický případ: chcete povolit navigaci v rámci vaší domény, ale externí odkazy otevřít v Safari.
class AppNavigationDecider: WebPage.NavigationDeciding {
let allowedHost: String
init(allowedHost: String) {
self.allowedHost = allowedHost
}
func decidePolicy(
for action: WebPage.NavigationAction,
preferences: inout WebPage.NavigationPreferences
) async -> WKNavigationActionPolicy {
guard let url = action.request.url else { return .cancel }
// Povolíme navigaci v rámci naší domény
if url.host == allowedHost {
return .allow
}
// Externí odkazy otevřeme v Safari
await UIApplication.shared.open(url)
return .cancel
}
}
struct ControlledWebView: View {
@State private var page: WebPage
init() {
let decider = AppNavigationDecider(allowedHost: "www.swift.org")
_page = State(initialValue: WebPage(navigationDecider: decider))
}
var body: some View {
WebView(page)
.ignoresSafeArea()
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
NavigationDeciding umožňuje specifikovat politiky pro různé fáze navigace — před zahájením, po obdržení odpovědi nebo při potřebě autentizace. Pro hybridní aplikace je to neocenitelné.
Vlastní URL schémata
Pro pokročilé hybridní aplikace nabízí WebPage podporu vlastních URL schémat přes protokol URLSchemeHandler. Můžete tak načítat lokální zdroje přes vlastní schéma (a vyhnout se bezpečnostním omezením u file://):
struct LocalContentSchemeHandler: URLSchemeHandler {
func reply(
for request: URLRequest
) -> some AsyncSequence<URLSchemeTaskResult, any Error> {
AsyncThrowingStream { continuation in
guard
let fileName = request.url?.host,
let bundleURL = Bundle.main.url(
forResource: fileName,
withExtension: nil
),
let data = try? Data(contentsOf: bundleURL)
else {
continuation.finish(
throwing: URLError(.badURL)
)
return
}
let response = URLResponse(
url: request.url!,
mimeType: "text/html",
expectedContentLength: data.count,
textEncodingName: "utf-8"
)
continuation.yield(.response(response))
continuation.yield(.data(data))
continuation.finish()
}
}
}
struct CustomSchemeView: View {
@State private var page: WebPage
init() {
let scheme = URLScheme("moje-app")!
var config = WebPage.Configuration()
config.urlSchemeHandlers[scheme] = LocalContentSchemeHandler()
_page = State(initialValue: WebPage(configuration: config))
}
var body: some View {
WebView(page)
.onAppear {
if let url = URL(string: "moje-app://index.html") {
page.load(URLRequest(url: url))
}
}
}
}
Vlastní URL schémata se hodí hlavně pro:
- Načítání lokálního HTML/CSS/JS obsahu z bundle
- Offline-first hybridní aplikace
- Bezpečné oddělení lokálních zdrojů od vzdáleného obsahu
Konfigurace WebPage
Třída WebPage.Configuration umožňuje přizpůsobit chování webového obsahu ještě před jeho načtením:
var config = WebPage.Configuration()
// Přizpůsobení user agent řetězce
config.applicationNameForUserAgent = "MojeAplikace/1.0"
// Povolení AirPlay pro média
config.allowsAirPlayForMediaPlayback = true
// Detekce typů dat (telefony, adresy, odkazy)
config.dataDetectorTypes = [.phoneNumber, .link, .address]
let page = WebPage(configuration: config)
Důležité — konfiguraci musíte nastavit před vytvořením instance WebPage. Po inicializaci ji už nelze měnit.
Sledování scroll pozice
Modifikátor webViewScrollPosition umožňuje napojit pozici scrollu na SwiftUI ScrollPosition. To otevírá zajímavé možnosti — třeba synchronizaci scrollu s dalšími UI prvky nebo vytvoření „scroll to top" tlačítka:
struct ScrollTrackingWebView: View {
@State private var page = WebPage()
@State private var scrollPosition = ScrollPosition()
@State private var currentOffset: CGFloat = 0
var body: some View {
VStack {
Text("Scroll offset: \(Int(currentOffset)) px")
.font(.caption)
.padding(.horizontal)
WebView(page)
.webViewScrollPosition($scrollPosition)
.webViewOnScrollGeometryChange(
for: CGFloat.self,
of: \.contentOffset.y
) { _, newValue in
currentOffset = newValue
}
.ignoresSafeArea()
}
.onAppear {
if let url = URL(string: "https://www.swift.org/documentation/") {
page.load(URLRequest(url: url))
}
}
}
}
Programatický scroll na konkrétní pozici je taky jednoduchý:
withAnimation(.easeInOut(duration: 0.25)) {
scrollPosition.scrollTo(y: 500)
}
Sledování navigačních událostí
Pro komplexnější scénáře má WebPage property navigations — je to AsyncSequence navigačních událostí. Můžete tak reagovat na různé fáze navigace a třeba zobrazit stavovou zprávu:
struct EventTrackingWebView: View {
@State private var page = WebPage()
@State private var statusMessage = "Připraven"
var body: some View {
VStack {
Text(statusMessage)
.font(.caption)
.foregroundStyle(.secondary)
WebView(page)
.ignoresSafeArea()
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
.task {
await observeNavigation()
}
}
private func observeNavigation() async {
let eventStream = Observations { page.navigations }
for await observation in eventStream {
do {
for try await event in observation {
switch event {
case .startedProvisionalNavigation:
statusMessage = "Načítání..."
case .committed:
statusMessage = "Vykreslování obsahu..."
case .finished:
statusMessage = "Hotovo"
case .receivedServerRedirect:
statusMessage = "Přesměrování..."
@unknown default:
break
}
}
} catch {
statusMessage = "Chyba: \(error.localizedDescription)"
}
}
}
}
WebView vs WKWebView vs SFSafariViewController: Kdy co použít
S příchodem nativního WebView máte teď tři možnosti pro zobrazení webového obsahu. Každá má své místo a je dobré vědět, kdy sáhnout po které:
| Vlastnost | SwiftUI WebView | WKWebView | SFSafariViewController |
|---|---|---|---|
| Nativní SwiftUI | Ano | Vyžaduje UIViewRepresentable | Vyžaduje UIViewControllerRepresentable |
| Přizpůsobitelné UI | Ano | Plná kontrola | Minimální |
| Spouštění JavaScriptu | Ano (přes WebPage) | Ano | Ne |
| Sdílení cookies se Safari | Ne | Ne | Ano |
| Password AutoFill | Ne | Ne | Ano |
| Minimální iOS verze | iOS 26 | iOS 8 | iOS 9 |
| Podpora macOS | Ano (macOS 26) | Ano | Ne |
Takže v kostce:
- SwiftUI WebView — pro nové aplikace cílící na iOS 26+, kde chcete čistý deklarativní kód
- WKWebView — pokud potřebujete podporu starších verzí iOS nebo plnou kontrolu nad konfigurací
- SFSafariViewController — když chcete poskytnout kompletní Safari zážitek včetně sdílených cookies a doplňování hesel (typicky pro OAuth přihlašování)
Migrace z WKWebView na nativní WebView
Pokud máte existující kód s UIViewRepresentable wrapperem kolem WKWebView, migrace je překvapivě přímočará. Podívejte se na ten rozdíl:
Starý přístup (před iOS 26)
// Starý přístup — desítky řádků boilerplate kódu
struct LegacyWebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
webView.load(URLRequest(url: url))
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, WKNavigationDelegate {
func webView(
_ webView: WKWebView,
didFinish navigation: WKNavigation!
) {
// Stránka načtena
}
func webView(
_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error
) {
// Chyba
}
}
}
Nový přístup (iOS 26+)
// Nový přístup — čistý, deklarativní, bezpečný
struct ModernWebView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
page.load(URLRequest(url: URL(string: "https://www.swift.org")!))
}
}
}
Méně kódu, žádné koordinátory, žádné delegáty, a plná typová bezpečnost díky Swift concurrency. Upřímně, tohle je jeden z těch případů, kdy nové API opravdu stojí za ten přechod.
Praktický příklad: Mini prohlížeč
Na závěr si pojďme postavit kompletní mini prohlížeč, který kombinuje všechny techniky z tohoto článku. Je to trochu delší kus kódu, ale výsledek stojí za to:
struct MiniBrowserView: View {
@State private var page = WebPage()
@State private var urlText = "https://www.swift.org"
@State private var showFind = false
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Adresní řádek
HStack {
TextField("URL adresa", text: $urlText)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.onSubmit {
loadURL()
}
Button("Jít") {
loadURL()
}
.buttonStyle(.bordered)
}
.padding(.horizontal)
.padding(.vertical, 8)
// Progress bar
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.progressViewStyle(.linear)
}
// Webový obsah
WebView(page)
.webViewBackForwardNavigationGestures(.enabled)
.webViewMagnificationGestures(.enabled)
.findNavigator(isPresented: $showFind)
.ignoresSafeArea(edges: .bottom)
}
.navigationTitle(page.title ?? "Prohlížeč")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
if let item = page.backForwardList.backItem {
page.load(item)
}
}) {
Image(systemName: "chevron.left")
}
.disabled(page.backForwardList.backItem == nil)
Button(action: {
if let item = page.backForwardList.forwardItem {
page.load(item)
}
}) {
Image(systemName: "chevron.right")
}
.disabled(page.backForwardList.forwardItem == nil)
Spacer()
Button(action: { showFind.toggle() }) {
Image(systemName: "doc.text.magnifyingglass")
}
if page.isLoading {
Button(action: { page.stopLoading() }) {
Image(systemName: "xmark")
}
} else {
Button(action: { page.reload() }) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}
.onAppear {
loadURL()
}
}
private func loadURL() {
var finalURL = urlText
if !finalURL.hasPrefix("http://") && !finalURL.hasPrefix("https://") {
finalURL = "https://" + finalURL
}
if let url = URL(string: finalURL) {
page.load(URLRequest(url: url))
}
}
}
Adresní řádek, progress bar, navigace zpět/vpřed, obnovení stránky, hledání na stránce a podpora gest. To vše v čistém SwiftUI — bez jediného řádku UIKit kódu. Kdyby mi to někdo řekl před pár lety, nevěřil bych.
Často kladené otázky
Mohu použít SwiftUI WebView na starších verzích iOS?
Bohužel ne. Nativní WebView je dostupný pouze od iOS 26 (macOS 26, visionOS 3). Pro starší verze musíte nadále používat WKWebView zabalený v UIViewRepresentable. Dobrá zpráva je, že oba přístupy mohou koexistovat ve stejném projektu — stačí použít #available(iOS 26, *) pro podmíněné využití nového API.
Jaký je rozdíl mezi WebView a WebPage?
WebView je SwiftUI view — vizuální komponenta, která zobrazuje webový obsah na obrazovce. WebPage je Observable model, který ten obsah representuje a dává vám API pro jeho ovládání. Jednoduchý WebView(url:) stačí pro statické zobrazení, ale pro jakoukoliv interakci potřebujete WebPage.
Podporuje nový WebView spouštění JavaScriptu?
Ano, přes třídu WebPage a metodu callJavaScript(_:arguments:). Je asynchronní a throwable, takže se přirozeně integruje se Swift concurrency. Výsledek je typu Any?, který musíte přetypovat na očekávaný typ. Podporuje taky pojmenované argumenty, což je příjemné pro čitelnost kódu.
Mohu ve WebView zobrazit lokální HTML soubory?
Jasně. WebPage podporuje načítání lokálního HTML přes metodu load(html:baseURL:). Jako baseURL doporučuju použít Bundle.main.resourceURL, což umožní HTML odkazovat na další soubory v bundle (CSS, obrázky, skripty). Pro sofistikovanější řešení můžete využít vlastní URL schémata přes URLSchemeHandler.
Jak se SwiftUI WebView liší od SFSafariViewController?
SFSafariViewController poskytuje kompletní Safari zážitek — sdílení cookies, automatické doplňování hesel, blokování obsahu. Nedá se ale přizpůsobit ani z něj extrahovat data. SwiftUI WebView naopak nabízí plnou kontrolu nad zobrazením a interakcí, podporuje JavaScript a je nativně integrován do SwiftUI. Jednoduché pravidlo: SFSafariViewController pro zobrazení externích webů (třeba OAuth login), WebView pro integraci webového obsahu do vaší aplikace.