SwiftUI WebView in iOS 26: So bindest du Webinhalte nativ ein

Mit iOS 26 bekommt SwiftUI endlich einen nativen WebView — ohne UIKit-Bridge. Dieser Guide zeigt dir alles: von der URL-Anzeige über WebPage und JavaScript-Ausführung bis hin zu eigenen URL-Schemata und Navigation.

Endlich: Ein nativer WebView für SwiftUI

Hand aufs Herz — wer in den letzten Jahren Webinhalte in einer SwiftUI-App anzeigen wollte, kennt den Schmerz. Es gab schlicht keinen nativen Weg. Du musstest auf UIViewRepresentable zurückgreifen, einen WKWebView aus UIKit einwickeln und dich mit State-Synchronisation, Delegate-Callbacks und einer ganzen Menge Boilerplate herumschlagen. Das hat funktioniert, klar. Aber schön war es nie.

Mit iOS 26 ist das endlich Geschichte.

Das neue WebView — zusammen mit dem Companion-Modell WebPage — ist eine vollständig native SwiftUI-Komponente. Kein UIKit-Bridge mehr, keine makeUIView/updateUIView-Tänze. Das Ganze fügt sich so nahtlos in das deklarative SwiftUI-Paradigma ein, dass man sich ehrlich fragt, warum es das nicht schon viel früher gab.

In diesem Guide zeige ich dir Schritt für Schritt, wie du WebView und WebPage in deiner App einsetzt — von der simplen URL-Anzeige bis hin zu JavaScript-Ausführung, eigenen URL-Schemata und Navigation. Also, los geht's.

Voraussetzungen

Bevor wir loslegen, kurz die technischen Anforderungen:

  • Betriebssystem: iOS 26+, macOS 26+ (Tahoe) oder visionOS 26+
  • Xcode: Version 26 oder neuer
  • Framework: import WebKit — das WebKit-Framework wurde um die neuen SwiftUI-APIs erweitert
  • Minimum Deployment Target: iOS 26 (kein Backport auf ältere Versionen)

Kurz gesagt: Du brauchst das Neueste vom Neuen. Das WebView basiert übrigens auf derselben WebKit-Engine, die auch Safari antreibt. Du bekommst also denselben Rendering-Unterbau — nur eben mit einer deklarativen SwiftUI-API darüber.

Einfache Nutzung: Eine URL laden

Der simpelste Anwendungsfall? Denkbar unkompliziert. Du importierst WebKit und übergibst eine URL:

import SwiftUI
import WebKit

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

Das war's. Eine Zeile Code für ein vollwertiges Web-Browsing-Erlebnis in deiner App. Die Seite wird geladen, der Nutzer kann scrollen und mit Inhalten interagieren — alles nativ in SwiftUI.

Für viele Anwendungsfälle reicht das schon völlig aus. Hilfe-Seite? AGB-Anzeige? Eingebetteter Blog? Erledigt.

WebPage: Volle Kontrolle über den Webinhalt

Wenn du mehr brauchst als nur eine URL-Anzeige — etwa den Seitentitel anzeigen, den Ladefortschritt verfolgen oder JavaScript ausführen — kommt WebPage ins Spiel. Das ist quasi das Herzstück hinter dem WebView: ein Observable-Modell, das dir Zugriff auf den gesamten Zustand der Webseite gibt.

import SwiftUI
import WebKit

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

    var body: some View {
        VStack(spacing: 0) {
            // Ladefortschritt als ProgressView
            if page.isLoading {
                ProgressView(value: page.estimatedProgress)
                    .tint(.blue)
            }

            WebView(page)
                .ignoresSafeArea(edges: .bottom)
        }
        .navigationTitle(page.title ?? "Laden...")
        .onAppear {
            if let url = URL(string: "https://developer.apple.com/swift/") {
                page.load(URLRequest(url: url))
            }
        }
    }
}

Da WebPage dem Observable-Protokoll entspricht, aktualisiert sich die UI automatisch, sobald sich Eigenschaften wie title, url, isLoading oder estimatedProgress ändern. Das ist ein riesiger Unterschied zum alten WKWebView-Delegate-Pattern, wo man Änderungen manuell über KVO oder Delegates propagieren musste. Wer das mal gemacht hat, weiß wie nervig das werden konnte.

Die wichtigsten WebPage-Eigenschaften

  • title — Der aktuelle Seitentitel
  • url — Die aktuelle URL
  • isLoading — Ob die Seite gerade lädt
  • estimatedProgress — Ladefortschritt als Double (0.0 bis 1.0)
  • backForwardList — Der Browserverlauf für Vor-/Zurück-Navigation
  • hasOnlySecureContent — Ob die Seite ausschließlich über HTTPS geladen wurde

Vor- und Zurück-Navigation implementieren

Ein Detail, das man vielleicht nicht sofort erwartet: Das neue SwiftUI-WebView bringt keine eingebauten Navigationstasten mit. Du musst Zurück- und Vorwärts-Buttons selbst bauen. Klingt erst mal nach Mehraufwand, hat aber einen klaren Vorteil — du bestimmst das Design komplett selbst.

import SwiftUI
import WebKit

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

    var body: some View {
        VStack(spacing: 0) {
            // Adressleiste
            HStack {
                TextField("URL eingeben", text: $urlText)
                    .textFieldStyle(.roundedBorder)
                    .autocapitalization(.none)
                    .keyboardType(.URL)
                    .onSubmit { ladeURL() }

                Button("Los") { ladeURL() }
                    .buttonStyle(.borderedProminent)
            }
            .padding(.horizontal)
            .padding(.vertical, 8)

            // Ladebalken
            if page.isLoading {
                ProgressView(value: page.estimatedProgress)
                    .tint(.accentColor)
            }

            // WebView
            WebView(page)
                .ignoresSafeArea(edges: .bottom)
        }
        .toolbar {
            ToolbarItemGroup(placement: .bottomBar) {
                // Zurück-Button
                Button {
                    if let letzter = page.backForwardList.backList.last {
                        page.load(letzter)
                    }
                } label: {
                    Image(systemName: "chevron.backward")
                }
                .disabled(page.backForwardList.backList.isEmpty)

                // Vorwärts-Button
                Button {
                    if let naechster = page.backForwardList.forwardList.first {
                        page.load(naechster)
                    }
                } label: {
                    Image(systemName: "chevron.forward")
                }
                .disabled(page.backForwardList.forwardList.isEmpty)

                Spacer()

                // Neu laden
                Button {
                    page.reload()
                } label: {
                    Image(systemName: "arrow.clockwise")
                }

                // Teilen
                if let url = page.url {
                    ShareLink(item: url)
                }
            }
        }
        .onAppear { ladeURL() }
    }

    private func ladeURL() {
        guard let url = URL(string: urlText) else { return }
        page.load(URLRequest(url: url))
    }
}

Der Schlüssel liegt in page.backForwardList: Die backList enthält alle zuvor besuchten Seiten, die forwardList alle Seiten, zu denen man vorwärts navigieren kann. Mit page.load(_:) springst du direkt zu einem Eintrag aus dem Verlauf.

Kleiner Tipp am Rande: Du kannst auch ein Menu statt eines einfachen Buttons verwenden, um dem Nutzer den gesamten Browserverlauf als Liste anzuzeigen — genau wie Safari das macht. Sieht deutlich professioneller aus.

JavaScript ausführen

Jetzt wird's richtig spannend. Eine der mächtigsten Funktionen von WebPage ist die Möglichkeit, JavaScript direkt auf der geladenen Seite auszuführen. Das eröffnet wirklich unzählige Möglichkeiten — von DOM-Manipulation über Datenextraktion bis hin zu Web-App-Integrationen.

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

    var body: some View {
        VStack {
            WebView(page)
                .onAppear {
                    if let url = URL(string: "https://www.swift.org/documentation/") {
                        page.load(URLRequest(url: url))
                    }
                }

            // Überschriften aus der Seite extrahieren
            Button("Überschriften extrahieren") {
                Task {
                    do {
                        let ergebnis = try await page.callJavaScript("""
                            Array.from(document.querySelectorAll('h2')).map(h => h.textContent).join('||')
                        """)
                        if let text = ergebnis as? String {
                            ueberschriften = text.components(separatedBy: "||")
                        }
                    } catch {
                        print("JS-Fehler: \(error)")
                    }
                }
            }
            .buttonStyle(.borderedProminent)
            .padding()

            List(ueberschriften, id: \.self) { titel in
                Text(titel)
            }
        }
    }
}

Die Methode callJavaScript(_:) ist asynchron und kann einen Rückgabewert liefern. Du kannst damit im Grunde beliebigen JavaScript-Code auf der Seite ausführen — ideal für Hybrid-Apps, die native und Web-Elemente kombinieren. Ich persönlich finde diesen Ansatz deutlich eleganter als die alten evaluateJavaScript-Callbacks aus der WKWebView-Welt.

Lokale HTML-Inhalte laden

WebPage kann nicht nur Remote-URLs laden, sondern auch lokale HTML-Inhalte direkt als String. Das ist perfekt für Offline-Inhalte, dynamisch generierte Seiten oder wenn du einfach formatierten Text anzeigen willst:

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

    let htmlInhalt = """
    <!DOCTYPE html>
    <html lang="de">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
            body { font-family: -apple-system; padding: 20px; background: #f5f5f7; }
            h1 { color: #1d1d1f; }
            .card { background: white; border-radius: 12px; padding: 20px; margin-top: 16px; }
        </style>
    </head>
    <body>
        <h1>Willkommen</h1>
        <div class="card">
            <p>Dieser Inhalt wird lokal aus einem HTML-String geladen.</p>
        </div>
    </body>
    </html>
    """

    var body: some View {
        WebView(page)
            .onAppear {
                page.loadHTML(htmlInhalt, baseURL: nil)
            }
    }
}

Mit loadHTML(_:baseURL:) lädst du den HTML-String direkt. Die optionale baseURL wird dann wichtig, wenn dein HTML relative Pfade für Bilder oder CSS enthält — sie definiert den Basispfad für diese Ressourcen. Ohne baseURL werden relative Pfade einfach nicht aufgelöst (ein Detail, das man gerne mal übersieht).

Eigene URL-Schemata mit URLSchemeHandler

Für fortgeschrittenere Anwendungsfälle — etwa wenn du App-gebundene Ressourcen über ein eigenes URL-Schema laden willst — bietet iOS 26 das URLSchemeHandler-Protokoll. Damit kannst du komplett eigene Protokolle definieren und die Inhalte selbst bereitstellen. Klingt kompliziert, ist es aber gar nicht so sehr:

import WebKit

struct AppContentHandler: URLSchemeHandler {
    static let scheme = "appcontent"

    func reply(to request: URLRequest) async throws -> some AsyncSequence<URLSchemeHandler.Event, any Error> {
        AsyncThrowingStream { continuation in
            guard let url = request.url,
                  let pfad = url.host() else {
                continuation.finish(throwing: URLError(.badURL))
                return
            }

            // Inhalte aus dem App-Bundle laden
            if let dateiPfad = Bundle.main.path(forResource: pfad, ofType: "html"),
               let daten = FileManager.default.contents(atPath: dateiPfad) {
                let antwort = URLResponse(
                    url: url,
                    mimeType: "text/html",
                    expectedContentLength: daten.count,
                    textEncodingName: "utf-8"
                )
                continuation.yield(.response(antwort, daten))
            }
            continuation.finish()
        }
    }
}

// Verwendung in der View
struct AppContentView: View {
    @State private var page = WebPage(
        configuration: .init(urlSchemeHandlers: [AppContentHandler()])
    )

    var body: some View {
        WebView(page)
            .onAppear {
                if let url = URL(string: "appcontent://hilfe") {
                    page.load(URLRequest(url: url))
                }
            }
    }
}

Das ist besonders praktisch, wenn du HTML-, CSS- und JavaScript-Dateien im App-Bundle mitliefern möchtest und diese über ein eigenes Schema wie appcontent:// adressieren willst. Dein Handler entscheidet dann, welche Daten zurückgegeben werden — maximale Flexibilität, ohne den Umweg über einen lokalen Server.

Navigation steuern mit NavigationDeciding

Manchmal willst du kontrollieren, welche Links innerhalb des WebViews geöffnet werden und welche den externen Browser aufrufen sollen. Ein typisches Szenario: Interne Links sollen im WebView bleiben, alles andere geht an Safari. Dafür gibt's das NavigationDeciding-Protokoll:

struct AppNavigationsRegeln: WebPage.NavigationDeciding {
    let erlaubteDomains: Set<String> = ["apple.com", "developer.apple.com", "swift.org"]

    func policy(
        for navigationAction: NavigationAction,
        preferences: inout NavigationPreferences
    ) -> NavigationActionPolicy {
        guard let url = navigationAction.request.url,
              let host = url.host() else {
            return .cancel
        }

        // Nur erlaubte Domains im WebView laden
        if erlaubteDomains.contains(where: { host.hasSuffix($0) }) {
            return .allow
        }

        // Alle anderen Links extern öffnen
        UIApplication.shared.open(url)
        return .cancel
    }
}

Saubere Trennung. Interne Links bleiben drin, externe Links öffnen Safari. Das verbessert die Nutzererfahrung enorm und verhindert, dass Nutzer versehentlich in deinem App-Browser auf irgendwelchen fremden Seiten landen.

Abwärtskompatibilität: Ältere iOS-Versionen unterstützen

Da das neue WebView erst ab iOS 26 verfügbar ist, brauchst du für Apps mit älterem Deployment Target natürlich einen Fallback. Der bewährte Ansatz ist simpel:

struct KompatiblerWebView: View {
    let url: URL

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

// Fallback für ältere iOS-Versionen
struct LegacyWebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.load(URLRequest(url: url))
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {}
}

So nutzt du die neue API, wo sie verfügbar ist, und unterstützt gleichzeitig ältere Geräte. Sobald dein Minimum Deployment Target irgendwann auf iOS 26 angehoben wird, fliegt der Legacy-Code einfach raus.

WebView vs. WKWebView vs. SFSafariViewController: Wann was?

Mit iOS 26 stehen dir jetzt drei Optionen zur Verfügung. Hier eine kurze Orientierungshilfe:

  • SwiftUI WebView: Erste Wahl für alle neuen SwiftUI-Apps mit iOS 26+. Deklarativ, sauber, voll integriert ins SwiftUI-Ökosystem. Wenn du die Wahl hast, nimm das.
  • WKWebView (UIKit): Weiterhin relevant für UIKit-Apps oder wenn du Features brauchst, die das neue WebView noch nicht abdeckt — etwa tiefgreifende WKWebViewConfiguration-Anpassungen.
  • SFSafariViewController: Wenn du eine vollwertige Safari-Erfahrung bieten willst, inklusive Adressleiste, Lesezeichen und Auto-Fill. Ideal für OAuth-Flows oder wenn der Nutzer die volle Browser-Funktionalität erwartet.

Bekannte Einschränkungen

So gelungen die neue API auch ist — es gibt ein paar Punkte, die du im Hinterkopf behalten solltest:

  • Keine eingebaute Navigation: Zurück/Vorwärts-Buttons musst du selbst implementieren (wie oben gezeigt)
  • Kein direkter Zugriff auf WKWebViewConfiguration: Für erweiterte Konfigurationen wie Custom User Agents oder User Scripts brauchst du unter Umständen noch den UIKit-Weg
  • Layout-Probleme: Einige Entwickler berichten von Layout-Bugs bei Verwendung von .ignoresSafeArea() — teste hier gründlich auf verschiedenen Geräten
  • Nur iOS 26+: Kein Backport auf ältere Versionen, ein Fallback ist also Pflicht, wenn du ältere Geräte unterstützen musst

Apple wird hier vermutlich in den nächsten Releases nachbessern. Für die allermeisten Anwendungsfälle reicht die aktuelle API aber schon jetzt locker aus.

Häufig gestellte Fragen

Wie unterscheidet sich das neue SwiftUI WebView von WKWebView?

Das neue WebView ist eine native SwiftUI-Komponente, die keine UIKit-Bridge über UIViewRepresentable mehr benötigt. Es nutzt dieselbe WebKit-Engine wie WKWebView, bietet aber eine deklarative API und arbeitet nahtlos mit dem Observable-Framework zusammen. Der größte Vorteil: weniger Boilerplate, automatische State-Synchronisation und ein insgesamt deutlich aufgeräumterer Code.

Kann ich das SwiftUI WebView auch für ältere iOS-Versionen nutzen?

Leider nein. Das native WebView ist ausschließlich ab iOS 26 verfügbar. Für ältere Versionen musst du weiterhin WKWebView mit UIViewRepresentable einwickeln. Am besten nutzt du einen bedingten Fallback mit if #available(iOS 26, *), wie weiter oben im Abschnitt zur Abwärtskompatibilität gezeigt.

Wie führe ich JavaScript im neuen WebView aus?

Über die Methode callJavaScript(_:) des WebPage-Modells. Die ist asynchron und kann Rückgabewerte liefern. Du kannst damit beliebigen JavaScript-Code auf der geladenen Seite ausführen — perfekt für DOM-Manipulation, Datenextraktion oder die Kommunikation zwischen nativer App und Web-Inhalten.

Funktioniert das WebView auch mit lokalen HTML-Dateien?

Ja, absolut. WebPage bietet die Methode loadHTML(_:baseURL:), mit der du HTML-Strings direkt laden kannst. Für gebundelte Dateien im App-Bundle kannst du zusätzlich eigene URL-Schemata über das URLSchemeHandler-Protokoll definieren und so lokale Ressourcen wie HTML, CSS und JavaScript adressieren.

Unterstützt das neue WebView Vor- und Zurück-Navigation?

Ja, allerdings musst du die Navigationsbuttons selbst implementieren. Das WebPage-Modell stellt über backForwardList den vollständigen Browserverlauf bereit. Mit page.load(_:) navigierst du direkt zu einem Eintrag aus der Back- oder Forward-Liste — du hast also volle Kontrolle über das Design deiner Navigation.

Über den Autor Editorial Team

Our team of expert writers and editors.