Varför Nativ WebView i SwiftUI Förändrar Allt
Okej, låt oss vara ärliga. I åratal har SwiftUI-utvecklare kämpat med att visa webbinnehåll i sina appar, och lösningen var alltid densamma: slå in UIKits WKWebView i en UIViewRepresentable-wrapper, hantera koordinatorer, delegates och en hel drös med boilerplate-kod. Det funkade — men det var verkligen inte kul att underhålla.
Med iOS 26 ändrade Apple spelplanen helt. WWDC 2025 introducerade en helt nativ WebView direkt i SwiftUI, byggd från grunden för att fungera sömlöst med SwiftUIs deklarativa paradigm. Inget mer UIViewRepresentable. Inga koordinatorer. Bara ren, deklarativ Swift-kod.
Ärligt talat — det var på tiden.
I den här guiden tar vi dig genom hela resan: från att visa en enkel webbsida till att bygga en fungerande miniwebbläsare med navigeringskontroller, JavaScript-integration, laddningsindikator och anpassade navigeringspolicyer. Varje avsnitt innehåller kodexempel som du kan använda direkt i dina egna projekt.
Grunderna: Din Första WebView
Importera WebKit-ramverket
Det nya WebView API:et lever i WebKit-ramverket, så du behöver importera både SwiftUI och WebKit:
import SwiftUI
import WebKit
WebView finns tillgängligt från och med iOS 26, macOS 26, visionOS 3 och watchOS 26. Inga externa beroenden — det ingår direkt i SDK:n.
Visa en webbsida med en enda rad kod
Det enklaste sättet att visa webbinnehåll i SwiftUI? Skapa en WebView med en URL:
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.apple.com")!)
.ignoresSafeArea()
}
}
Det är bokstavligen allt. En enda rad ger dig en komplett webbvisning med scrollning, länkar, video och JavaScript — allt fungerar direkt ur lådan. Jämför det med de 30–40 rader boilerplate som krävdes med den gamla UIViewRepresentable-metoden, och du förstår varför det här är en så stor grej.
Dynamiska URL:er med @State
I en riktig app vill du sällan visa en hårdkodad URL. Genom att använda @State gör du URL:en dynamisk:
struct DynamiskWebView: View {
@State private var url = URL(string: "https://www.apple.com")!
var body: some View {
VStack {
WebView(url: url)
HStack {
Button("Apple") {
url = URL(string: "https://www.apple.com")!
}
Button("Swift.org") {
url = URL(string: "https://www.swift.org")!
}
}
.padding()
}
}
}
När URL:en ändras uppdateras WebView automatiskt. Ren SwiftUI-reaktivitet — precis som det ska vara.
WebView vs. WebPage: Två Nivåer av Kontroll
Apple introducerade två centrala API:er för webbinnehåll i SwiftUI, och att förstå skillnaden mellan dem sparar dig en hel del huvudvärk:
- WebView — perfekt för enkla användningsfall där du bara behöver visa en webbsida. Minimal kod, maximal enkelhet.
- WebPage — ger dig djupare kontroll. Du kan spåra laddningsprogress, komma åt sidans titel och URL, köra JavaScript, hantera navigering och mycket mer.
Tumregeln: börja med WebView. Byt till WebPage när du märker att du behöver mer kontroll. Inte innan.
Avancerad Kontroll med WebPage
Grundläggande WebPage-setup
WebPage är en observerbar modell som representerar en webbsidas tillstånd. Eftersom den följer Observable-protokollet uppdateras ditt gränssnitt automatiskt när sidan ändras — och det känns riktigt smooth i praktiken:
struct AvanceradWebView: View {
@State private var page = WebPage()
var body: some View {
VStack {
WebView(page)
.ignoresSafeArea(.container, edges: .bottom)
VStack(alignment: .leading, spacing: 4) {
Text("Titel: \(page.title ?? "Laddar...")")
.font(.headline)
Text("URL: \(page.url?.absoluteString ?? "-")")
.font(.caption)
.lineLimit(1)
}
.padding(.horizontal)
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
Notera att vi skapar WebPage() som en @State-variabel och sedan binder den till WebView(page). Alla egenskaper — title, url, estimatedProgress — uppdateras reaktivt när användaren navigerar runt.
Laddningsindikator med ProgressView
WebPage exponerar egenskapen estimatedProgress som ger dig en ungefärlig procentuell laddningsstatus. Kombinera den med isLoading för en snygg förloppsindikator:
struct WebViewMedProgress: View {
@State private var page = WebPage()
var body: some View {
VStack(spacing: 0) {
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.tint(.blue)
}
WebView(page)
.ignoresSafeArea(.container, edges: .bottom)
}
.onAppear {
if let url = URL(string: "https://www.apple.com") {
page.load(URLRequest(url: url))
}
}
}
}
ProgressView visas överst i vyn och försvinner automatiskt när sidan laddats klart. Samma mönster som Safari och de flesta moderna webbläsare använder, så det känns omedelbart bekant för användaren.
Ladda om och stoppa laddning
Med WebPage får du enkla metoder för att ladda om eller avbryta en pågående laddning:
HStack {
Button("Ladda om") {
page.reload()
}
Button("Stoppa") {
page.stopLoading()
}
.disabled(!page.isLoading)
}
Inte raketvetenskap direkt, men det är ju det som är poängen. Det ska vara enkelt.
JavaScript-integration med callJavaScript
Det här är (enligt mig) en av de mest kraftfulla funktionerna i WebPage. Du kan köra JavaScript direkt från din Swift-kod. Metoden callJavaScript är asynkron och returnerar resultatet som en Swift-typ.
Grundläggande JavaScript-exekvering
struct JavaScriptExempel: View {
@State private var page = WebPage()
@State private var dokumentTitel = ""
var body: some View {
VStack {
WebView(page)
Button("Hämta titel via JS") {
Task {
do {
let titel = try await page.callJavaScript(
"document.title"
) as? String
dokumentTitel = titel ?? "Okänd"
} catch {
print("JS-fel: \(error)")
}
}
}
Text("JS-titel: \(dokumentTitel)")
.padding()
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
Skicka argument till JavaScript
Du kan skicka en dictionary med argument till callJavaScript, där nycklarna blir lokala JavaScript-variabler och värdena konverteras automatiskt:
Task {
try await page.callJavaScript(
"document.body.style.backgroundColor = color",
arguments: ["color": "lightblue"]
)
}
Det här är ett kraftfullt och typsäkert sätt att kommunicera mellan Swift och webbsidans DOM. Du slipper stränginpolering helt och hållet, vilket gör koden mycket renare (och säkrare).
Injicera lokalt HTML-innehåll
Förutom att ladda URL:er kan du också ladda in anpassat HTML direkt:
struct LokaltHTMLExempel: View {
@State private var page = WebPage()
let htmlInnehåll = """
<html>
<head>
<style>
body { font-family: -apple-system; padding: 20px; }
h1 { color: #007AFF; }
</style>
</head>
<body>
<h1>Hej från SwiftUI!</h1>
<p>Det här HTML-innehållet laddas direkt från Swift.</p>
</body>
</html>
"""
var body: some View {
WebView(page)
.onAppear {
page.loadHTML(htmlInnehåll)
}
}
}
Det här är perfekt för att visa lokalt genererat innehåll — villkor, kvitton, dynamisk HTML — utan att behöva en webbserver.
Navigering: Framåt, Bakåt och Historik
Tillbaka- och framåtnavigering
Till skillnad från den gamla WKWebView har det nya WebView inga inbyggda navigeringsknappar. Det kanske låter som en nackdel, men det ger dig full kontroll. Du implementerar dem själv med hjälp av WebPage och dess backForwardList:
struct WebbläsareNavigering: View {
@State private var page = WebPage()
var body: some View {
VStack(spacing: 0) {
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.tint(.blue)
}
WebView(page)
.ignoresSafeArea(.container, edges: .bottom)
}
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
Task {
try? await page.callJavaScript("history.back()")
}
}) {
Image(systemName: "chevron.backward")
}
.disabled(page.backForwardList.backList.isEmpty)
Button(action: {
Task {
try? await page.callJavaScript("history.forward()")
}
}) {
Image(systemName: "chevron.forward")
}
.disabled(page.backForwardList.forwardList.isEmpty)
Spacer()
Button(action: { page.reload() }) {
Image(systemName: "arrow.clockwise")
}
}
}
.onAppear {
if let url = URL(string: "https://www.apple.com") {
page.load(URLRequest(url: url))
}
}
}
}
Med backForwardList kan du kontrollera om det finns sidor att navigera bakåt eller framåt till och inaktivera knapparna därefter. Det ger en polerad upplevelse som användarna förväntar sig.
Navigeringshistorik med meny
Vill du ge användaren en komplett historiklista? Det löser du med en meny kopplad till navigeringsknappen:
Menu {
ForEach(page.backForwardList.backList.reversed(), id: \.url) { item in
Button(item.title ?? item.url.absoluteString) {
page.load(item)
}
}
} label: {
Image(systemName: "chevron.backward")
}
.disabled(page.backForwardList.backList.isEmpty)
Listan reverseras så att den senast besökta sidan visas överst — precis som i Safari. En liten detalj, men det är sådant som gör skillnad.
Anpassade Navigeringspolicyer
Ibland vill du styra vilka URL:er som öppnas i din WebView och vilka som ska öppnas externt i Safari. iOS 26 låter dig skapa en anpassad navigeringspolicy genom att implementera WebPage.NavigationDeciding-protokollet:
struct MinNavigeringsPolicy: WebPage.NavigationDeciding {
let tillatnaVärdar: Set<String>
func decidePolicyFor(
_ action: WebPage.NavigationAction
) async -> WebPage.NavigationPolicy {
guard let host = action.request.url?.host() else {
return .cancel
}
if tillatnaVärdar.contains(host) {
return .allow
}
// Öppna externa länkar i Safari
if let url = action.request.url {
await UIApplication.shared.open(url)
}
return .cancel
}
}
Sedan tillämpar du policyn på din WebPage:
let page = WebPage(
configuration: WebPage.Configuration(
navigationDecider: MinNavigeringsPolicy(
tillatnaVärdar: ["www.apple.com", "developer.apple.com"]
)
)
)
Det här mönstret är riktigt värdefullt för appar som visar specifikt webbinnehåll och vill förhindra att användaren navigerar iväg till oväntade sajter.
Sök på Sidan med Find Navigator
SwiftUI-versionen av WebKit inkluderar stöd för "Sök på sidan" direkt via en view modifier. Det ger användaren en bekant sökupplevelse som liknar Safaris inbyggda sökfunktion:
struct WebViewMedSök: View {
@State private var page = WebPage()
@State private var visarSök = false
var body: some View {
WebView(page)
.findNavigator(isPresented: $visarSök)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { visarSök.toggle() }) {
Image(systemName: "magnifyingglass")
}
}
}
.onAppear {
if let url = URL(string: "https://www.swift.org") {
page.load(URLRequest(url: url))
}
}
}
}
Modifieraren .findNavigator(isPresented:) visar en sökruta överst i webvyn. Användaren kan söka, navigera mellan träffar och stänga sökningen — allt utan att du behöver skriva en enda rad söklogik. Helt fantastiskt.
Bygga en Komplett Miniwebbläsare
Nu är det dags att sätta ihop allt vi lärt oss. Här bygger vi en komplett miniwebbläsare med adressfält, laddningsindikator, navigeringsknappar, sökning och delning:
struct Miniwebblasare: View {
@State private var page = WebPage()
@State private var urlText = "https://www.apple.com"
@State private var visarSök = false
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Laddningsindikator
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.tint(.accentColor)
}
// Webbvy
WebView(page)
.findNavigator(isPresented: $visarSök)
.ignoresSafeArea(.container, edges: .bottom)
}
.navigationTitle(page.title ?? "Webbläsare")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
// Adressfält
ToolbarItem(placement: .principal) {
HStack {
TextField("Ange URL", text: $urlText)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.keyboardType(.URL)
.onSubmit {
laddaURL()
}
}
}
// Navigeringskontroller
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
Task { try? await page.callJavaScript("history.back()") }
}) {
Image(systemName: "chevron.backward")
}
.disabled(page.backForwardList.backList.isEmpty)
Button(action: {
Task { try? await page.callJavaScript("history.forward()") }
}) {
Image(systemName: "chevron.forward")
}
.disabled(page.backForwardList.forwardList.isEmpty)
Spacer()
Button(action: { visarSök.toggle() }) {
Image(systemName: "magnifyingglass")
}
Button(action: { page.reload() }) {
Image(systemName: "arrow.clockwise")
}
if let url = page.url {
ShareLink(item: url)
}
}
}
.onAppear {
laddaURL()
}
}
}
private func laddaURL() {
var urlString = urlText.trimmingCharacters(in: .whitespaces)
if !urlString.hasPrefix("http://") && !urlString.hasPrefix("https://") {
urlString = "https://" + urlString
}
if let url = URL(string: urlString) {
page.load(URLRequest(url: url))
}
}
}
Den här miniwebbläsaren på under 80 rader kod ger dig:
- Adressfält med automatisk HTTPS-komplettering
- Visuell laddningsindikator
- Framåt- och bakåtnavigering
- Sök-på-sidan-funktion
- Sidtitel i navigationsfältet
- Delningsknapp för aktuell URL
- Ladda-om-funktion
Försök uppnå samma sak med den gamla UIViewRepresentable-metoden — du skulle behöva minst tre gånger så mycket kod, plus en separat koordinatorklass. Det säger ganska mycket om hur långt SwiftUI har kommit.
Bakåtkompatibilitet med Äldre iOS-versioner
Om din app fortfarande behöver stödja iOS-versioner äldre än 26 (och det gör de flesta appar just nu) kan du använda en villkorlig fallback:
struct AdaptivWebView: View {
let url: URL
var body: some View {
if #available(iOS 26, *) {
WebView(url: url)
} else {
LegacyWebView(url: url)
}
}
}
// Fallback för äldre iOS-versioner
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) {}
}
Det här ger dig det bästa av båda världar: nativa WebView på iOS 26 och uppåt, med en WKWebView-wrapper som fallback för äldre versioner. Smidigt.
Vanliga Misstag och Bästa Praxis
Här är de vanligaste fallgroparna jag sett bland utvecklare som börjar använda det nya WebView API:et — och hur du undviker dem:
- Ladda bara betrodda HTTPS-URL:er. Undvik att ladda godtyckliga URL:er utan validering. Kombinera gärna med en anpassad navigeringspolicy för maximal säkerhet.
- Var försiktig med JavaScript-injicering. Om du kör JavaScript baserat på användarinput, sanera alltid data först för att förhindra XSS-attacker. Använd
arguments-parametern istället för stränginpolering — den finns där av en anledning. - Tänk på minnesanvändning. Webbinnehåll kan vara överraskande minnesintensivt. Övervaka appens minnesanvändning i Instruments, särskilt om du laddar tunga webbsidor.
- Hantera felaktiga URL:er. Kontrollera alltid att
URL(string:)inte returnerarnilinnan du försöker ladda. Visa ett tydligt felmeddelande om URL:en är ogiltig. - Testa noggrant. Beta-versioner av iOS 26 har haft uppdateringar i
WebPage-navigeringen, så testa med den senaste SDK:n innan du skickar till App Store.
Vanliga Frågor
Vad är skillnaden mellan WebView och WKWebView i SwiftUI?
WebView är det helt nya nativa SwiftUI-API:et som introducerades i iOS 26. Det är byggt från grunden för SwiftUIs deklarativa modell och kräver ingen UIViewRepresentable-wrapper. WKWebView är det äldre UIKit-baserade API:et som fortfarande fungerar men kräver en brygga via UIViewRepresentable för att användas i SwiftUI. Bygger du en ny app för iOS 26 eller senare? Välj det nya WebView — det är inte ens en fråga.
Kan jag köra JavaScript i SwiftUIs nya WebView?
Absolut. Via WebPage-modellens callJavaScript-metod kan du exekvera godtycklig JavaScript-kod asynkront. Du kan skicka argument som en dictionary, där nycklarna blir lokala JavaScript-variabler. Metoden returnerar resultatet som en Swift-typ, vilket gör det enkelt att kommunicera mellan din app och webbsidan.
Hur hanterar jag cookies och autentisering i SwiftUI WebView?
Om du behöver hantera cookies (till exempel för att upprätthålla en inloggningssession) kan du konfigurera WebPage-objektets underliggande datakälla. Cookies från andra delar av din app, exempelvis URLSession, kan överföras till WebView via HTTPCookieStorage. Det viktigaste att komma ihåg: sätt cookies innan du laddar sidan för att undvika dubbla inloggningar.
Fungerar det nya WebView API:et på macOS och visionOS?
Ja, det gör det. WebView och WebPage finns tillgängliga på iOS 26, macOS 26 och visionOS 3. Samma kodbas fungerar på alla plattformar, vilket gör det ännu enklare att bygga multiplattformsappar med delat webbinnehåll.
Hur implementerar jag sök-på-sidan i SwiftUI WebView?
SwiftUI erbjuder view-modifieraren .findNavigator(isPresented:) som lägger till en komplett sökfunktion direkt i din WebView. Användaren kan söka efter text, navigera mellan träffar och stänga sökningen — allt utan att du behöver skriva någon söklogik alls. Bind bara en Bool-variabel till modifieraren och koppla den till en knapp i verktygsfältet.