WebView dans SwiftUI iOS 26 : le guide complet du nouveau composant natif
Le nouveau composant natif WebView de SwiftUI iOS 26 remplace enfin le wrapper UIViewRepresentable autour de WKWebView. Découvrez WebPage, la navigation observable, l'exécution JavaScript en async/await et la migration pratique.
WebView dans SwiftUI iOS 26 est un composant natif qui affiche du contenu web sans passer par UIViewRepresentable ni manipuler directement WKWebView. Combiné à l'objet observable WebPage, il pilote l'URL, la navigation, l'exécution JavaScript et les cookies depuis du Swift moderne. Honnêtement, si vous venez d'Objective-C et de l'ère UIWebView, c'est la première fois depuis 2014 que présenter une page web dans une app iOS redevient agréable.
WebView est disponible nativement en SwiftUI à partir d'iOS 26, iPadOS 26, macOS 26, visionOS 26 et tvOS 26.
WebPage est une classe @Observable qui expose url, title, estimatedProgress, isLoading et l'API de navigation.
L'exécution JavaScript se fait via callJavaScript(_:), une méthode async throws qui retourne un JSValue typé.
Les gestionnaires personnalisés (URLSchemeHandler) et les politiques de navigation sont déclarés directement sur la WebPage via des protocoles.
La migration depuis un wrapper WKWebView/UIViewRepresentable supprime généralement 60 à 80 % du code de coordination.
WebView SwiftUI : qu'est-ce qui change dans iOS 26 ?
Jusqu'à iOS 18, afficher une page web dans une vue SwiftUI imposait un détour familier. Vous emballiez WKWebView dans un UIViewRepresentable, vous exposiez un coordinateur pour WKNavigationDelegate, puis vous synchronisiez à la main l'URL, la progression et le titre. Ça marchait, mais le code sentait clairement l'héritage Objective-C. WKWebView est issu de la refonte WebKit2 de 2014, conçue à une époque où SwiftUI n'existait pas et où Combine n'avait pas encore inversé la pyramide des dépendances.
iOS 26 apporte une API SwiftUI de première classe : la vue WebView et le modèle observable WebPage. Apple a documenté l'ensemble dans la référence officielle WebKit pour SwiftUI. La nouveauté n'est pas qu'un sucre syntaxique. WebPage est conçu pour cohabiter avec @Observable, le système d'observation introduit en iOS 17 dont j'ai détaillé la migration dans mon guide complet sur @Observable et le framework Observation. En clair, les changements d'URL, de titre ou de progression invalident automatiquement les vues qui les lisent, sans @Published ni objectWillChange.
En pratique, le triptyque vue / modèle / configuration remplace la danse délégué/coordinateur. WebView est la vue, WebPage est le modèle, et la configuration (préférences JavaScript, gestionnaires d'URL, agent utilisateur) se passe à l'initialisation de WebPage via une WebPage.Configuration.
Comment intégrer un WebView dans SwiftUI
Le cas le plus simple, afficher une URL, tient en deux lignes utiles. Importez WebKit, instanciez une WebPage avec une URL initiale, et placez le WebView dans votre hiérarchie SwiftUI.
import SwiftUI
import WebKit
struct ReaderView: View {
@State private var page = WebPage(url: URL(string: "https://webkit.org")!)
var body: some View {
WebView(page)
.ignoresSafeArea()
}
}
La WebView remplit son conteneur, gère le défilement, le pinch-to-zoom et la gestuelle de balayage (swipe back) sans configuration. Pour ajouter une barre de progression et un titre dynamique, lisez les propriétés observables de la WebPage :
struct ReaderView: View {
@State private var page = WebPage(url: URL(string: "https://webkit.org")!)
var body: some View {
NavigationStack {
WebView(page)
.navigationTitle(page.title ?? "Chargement…")
.navigationBarTitleDisplayMode(.inline)
.overlay(alignment: .top) {
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.progressViewStyle(.linear)
}
}
}
}
}
WebPage et contrôle de la navigation
La WebPage expose les opérations classiques de navigation comme méthodes : load(_:), reload(), stopLoading(), plus goBack() et goForward(). Les propriétés canGoBack et canGoForward sont observables et peuvent piloter directement l'état d'activation des boutons de votre toolbar.
Pour intercepter les décisions de navigation (ouvrir les liens externes dans Safari, bloquer certains domaines, gérer les téléchargements), adoptez le protocole WebPage.NavigationDecider sur un type dédié et passez-le à la configuration :
Le résultat .cancelAndOpenInBrowser est une nouveauté très pratique : il délègue l'URL à UIApplication.open sur iOS ou à NSWorkspace sur macOS sans que vous ayez à le faire vous-même.
Comment exécuter du JavaScript dans un WebView SwiftUI
L'exécution JavaScript passe par callJavaScript(_:in:), une méthode async throws qui retourne un JSValue. Contrairement à l'ancien evaluateJavaScript(_:completionHandler:) de WKWebView, l'API moderne lève des erreurs typées (WebPage.JavaScriptError) et s'intègre proprement dans async/await et le modèle d'Approachable Concurrency de Swift 6.2.
Button("Extraire le titre H1") {
Task {
do {
let value = try await page.callJavaScript(
"document.querySelector('h1')?.innerText ?? ''"
)
if case .string(let title) = value {
print("H1 :", title)
}
} catch {
print("Échec JS :", error)
}
}
}
Pour injecter du script au chargement de chaque page (par exemple un thème sombre forcé), enregistrez un UserScript dans la configuration :
La communication bidirectionnelle JS vers Swift utilise toujours les WKScriptMessageHandler, mais désormais via une fermeture async au lieu d'un délégué :
config.userContentController.addScriptMessageHandler(name: "appBridge") { message in
if let payload = message.body as? [String: Any], let event = payload["event"] as? String {
await analytics.record(event)
}
}
Cookies, stockage et politiques de confidentialité
Le HTTPCookieStore est accessible via page.configuration.websiteDataStore.httpCookieStore, désormais asynchrone de bout en bout. Récupérer les cookies d'un domaine ne nécessite plus de callback :
let cookies = await page.configuration.websiteDataStore.httpCookieStore.allCookies()
let sessionCookies = cookies.filter { $0.domain.contains("webkit.org") }
Pour une session éphémère (équivalent du mode privé), construisez la configuration avec un WKWebsiteDataStore.nonPersistent(). C'est utile pour les flux OAuth où vous ne souhaitez pas que les identifiants survivent à la session :
let config = WebPage.Configuration()
config.websiteDataStore = .nonPersistent()
let oauthPage = WebPage(configuration: config, url: authURL)
Côté confidentialité, iOS 26 active par défaut la protection intelligente contre le pistage (ITP) et le partitionnement de stockage par origine. Vous n'avez plus à le configurer manuellement, mais soyez prévenu : un identifiant cross-site qui marchait dans un wrapper WKWebView mal configuré ne marchera plus. J'ai déjà perdu une demi-journée là-dessus sur une app interne, c'est le genre de régression silencieuse qu'on ne voit qu'en production.
URLSchemeHandler et schémas personnalisés
L'un des cas d'usage les plus utiles, servir des ressources locales depuis un bundle sans passer par un serveur HTTP, repose sur le protocole URLSchemeHandler. Vous déclarez un schéma (par exemple app-resource://) et vous fournissez un handler async qui retourne les données.
struct BundleResourceHandler: URLSchemeHandler {
func reply(for request: URLRequest) -> AsyncThrowingStream<URLSchemeTaskResult, Error> {
AsyncThrowingStream { continuation in
guard let path = request.url?.path,
let url = Bundle.main.url(forResource: path, withExtension: nil),
let data = try? Data(contentsOf: url) else {
continuation.finish(throwing: URLError(.fileDoesNotExist))
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()
}
}
}
config.urlSchemeHandlers[URLScheme("app-resource")!] = BundleResourceHandler()
L'usage d'AsyncThrowingStream permet de streamer des réponses par morceaux, pratique pour de gros assets ou des flux SSE. C'est aussi le mécanisme recommandé par l'équipe WebKit dans ses notes techniques pour les apps hybrides modernes.
Migration depuis WKWebView et UIViewRepresentable
Si votre app maintient un wrapper UIViewRepresentable autour de WKWebView, la migration suit un schéma assez mécanique. Voici un tableau comparatif des concepts.
Concept
WKWebView (avant iOS 26)
WebView SwiftUI (iOS 26+)
Vue dans SwiftUI
UIViewRepresentable manuel
WebView(page) natif
Modèle d'état
Coordinator + @Published
WebPage avec @Observable
Navigation déléguée
WKNavigationDelegate
WebPage.NavigationDecider
JavaScript
evaluateJavaScript + closure
callJavaScriptasync throws
Schémas personnalisés
WKURLSchemeHandler (ObjC-style)
URLSchemeHandler Swift natif
Cookies
Callbacks imbriqués
API async directe
iOS minimum
iOS 8+
iOS 26+ uniquement
La dernière ligne, c'est le piège de support. Si votre app cible iOS 17 ou plus ancien, vous devrez conserver le wrapper UIViewRepresentable en fallback via #available(iOS 26, *). Dans un de mes projets perso, j'ai isolé les deux implémentations derrière un protocole commun (WebViewSurface) et le choix se fait à un seul endroit. La majeure partie de l'app n'a rien à savoir du compilateur conditionnel.
Étapes de migration recommandées
Remplacez votre UIViewRepresentable par WebView(page) dans la vue feuille.
Convertissez votre ObservableObject coordinateur en passant les propriétés observées (URL, titre, progression) vers WebPage.
Transposez les méthodes de WKNavigationDelegate vers un type adoptant WebPage.NavigationDecider.
Remplacez les appels evaluateJavaScript par callJavaScript et basculez les sites d'appel en async.
Supprimez les @Published redondants, le framework Observation invalide les vues automatiquement.
Performance, pièges et bonnes pratiques
Une WebView reste un processus WebKit complet : sa création coûte plusieurs dizaines de millisecondes et consomme une part non négligeable de mémoire. Voilà quelques règles que je suis :
Réutilisez la WebPage entre les navigations plutôt que de la recréer. Mettez-la en @State au niveau du conteneur qui survit aux changements d'onglet.
Pré-chauffez les origines critiques via WebPage.warmUp(for:) avant que l'utilisateur n'arrive sur l'écran. La première navigation devient presque instantanée.
Méfiez-vous des fuites de tâches dans callJavaScript : les Task non annulées au démontage de la vue continuent à tenir des références fortes. Utilisez .task ou TaskGroup liés à la durée de vie de la vue.
Surveillez la mémoire avec Instruments → WebKit, surtout si vous chargez des pages riches en SVG ou Canvas. Le diagnostic dédié WebKit montre les processus de contenu et le pic mémoire par onglet.
Adoptez l'effet visuel Liquid Glass pour les overlays au-dessus du WebView : ils respectent les paramètres d'accessibilité et n'interfèrent pas avec le rendu WebKit.
Questions fréquentes
Quel est l'iOS minimum pour utiliser WebView en SwiftUI ?
La vue native WebView et la classe WebPage nécessitent iOS 26, iPadOS 26, macOS 26, visionOS 26 ou tvOS 26. Pour cibler une version antérieure, conservez votre wrapper UIViewRepresentable autour de WKWebView et basculez sous #available(iOS 26, *).
Quelle est la différence entre WebView SwiftUI et WKWebView ?
WebView est une vue SwiftUI ; WKWebView reste un UIView/NSView utilisable depuis UIKit. WebView s'appuie sur WebPage (observable, async/await) au lieu des délégués Objective-C. Sous le capot, les deux utilisent le même moteur WebKit.
Comment exécuter du JavaScript depuis une WebView SwiftUI ?
Appelez try await page.callJavaScript("expression"). La méthode retourne un JSValue typé (.string, .number, .boolean, .array, etc.). Pour injecter du script au chargement de chaque page, enregistrez un WebPage.UserScript dans la configuration.
Peut-on utiliser WebView pour un flux OAuth ?
Oui, mais préférez une session éphémère : initialisez WebPage.Configuration avec websiteDataStore = .nonPersistent() pour éviter que les cookies de session OAuth ne survivent. Surveillez l'URL via page.url pour détecter le callback du fournisseur et fermer la vue dès réception du code.
WebView fonctionne-t-il sur visionOS et tvOS ?
Oui. WebView est disponible sur visionOS 26 (avec gestion native du regard et du pincement) et sur tvOS 26 (navigation par focus). Sur watchOS, il n'y a pas de WebKit ; il faut toujours déléguer l'affichage web à l'iPhone associé.
Guide 2026 du framework Observation et de la macro @Observable en Swift : migration depuis ObservableObject, règles @State vs @Bindable, pièges concrets, withObservationTracking, type Observations (Swift 6.2) et SE-0506.
Apprenez à exposer les actions de votre app à Siri, Spotlight, Raccourcis et Apple Intelligence avec App Intents en Swift. Guide pas-à-pas avec exemples de code complets pour iOS 26.
Construisez un éditeur de texte riche 100 % natif SwiftUI sous iOS 26. Guide étape par étape avec TextEditor, AttributedString et barre d'outils de formatage — code fonctionnel inclus.