Mostrar contenido web ha sido durante años una de las ausencias más llamativas de SwiftUI. WebKit proporcionaba WKWebView, pero para integrarlo en una jerarquía declarativa había que envolverlo en UIViewRepresentable, crear un coordinador cuando necesitábamos delegados y conectar manualmente propiedades como el título o el progreso de carga.
iOS 26 elimina buena parte de esa infraestructura. WebKit incorpora ahora un WebView nativo para SwiftUI y lo acompaña de WebPage, un modelo observable que representa la página y su estado de navegación.
El caso sencillo cabe en una línea
Cuando solo necesitamos mostrar una página de ayuda, unas condiciones de uso o cualquier contenido que parte de una URL fija, podemos construir la vista directamente:
import SwiftUI
import WebKit
struct DocumentationView: View {
private let url = URL(
string: "https://www.swift.org/documentation/"
)!
var body: some View {
WebView(url: url)
.navigationTitle("Documentación")
.navigationBarTitleDisplayMode(.inline)
}
}
Ya no hay que implementar makeUIView ni updateUIView, y tampoco existe el riesgo habitual de volver a cargar la misma URL cada vez que SwiftUI actualiza el wrapper. WebView participa en la composición como cualquier otra vista y admite los modificadores habituales de SwiftUI.
Hay un detalle importante en los imports: el símbolo solo está disponible cuando el archivo importa a la vez SwiftUI y WebKit. Apple lo distribuye mediante un cross-import overlay, un módulo adicional que el compilador activa al combinar ambos. De esta forma, SwiftUI no necesita depender de WebKit ni WebKit convertir toda su API en una dependencia de SwiftUI; la integración vive en una capa separada.
WebPage separa la vista del estado
El inicializador con una URL resulta suficiente mientras no necesitemos conocer lo que ocurre dentro. En cuanto la interfaz debe mostrar el título real, el progreso, el historial o controles de navegación, entra en juego WebPage.
WebPage implementa Observation, por lo que puede conservarse con @State. SwiftUI registra las propiedades leídas desde body y vuelve a calcular la interfaz cuando cambian, sin KVO, delegados ni un coordinador intermedio.
import SwiftUI
import WebKit
@available(iOS 26.0, *)
struct HelpCenterView: View {
@State private var page = WebPage()
private let initialURL = URL(
string: "https://example.com/help"
)!
var body: some View {
WebView(page)
.navigationTitle(
page.title.isEmpty ? "Centro de ayuda" : page.title
)
.navigationBarTitleDisplayMode(.inline)
.safeAreaInset(edge: .top, spacing: 0) {
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.progressViewStyle(.linear)
}
}
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button("Atrás", systemImage: "chevron.backward") {
guard let item = page.backForwardList.backList.last else {
return
}
page.load(item)
}
.disabled(page.backForwardList.backList.isEmpty)
Spacer()
Button("Recargar", systemImage: "arrow.clockwise") {
page.reload()
}
}
}
.task {
page.load(URLRequest(url: initialURL))
}
}
}
Además de title, isLoading y estimatedProgress, el modelo expone la URL actual, backForwardList y hasOnlySecureContent. Esta última propiedad permite reflejar en la interfaz si todos los recursos de la página se han cargado mediante conexiones seguras, aunque no sustituye a una política propia sobre qué dominios y esquemas puede abrir la aplicación.
La carga también adopta concurrencia de Swift
WebPage puede cargar una URL, una URLRequest, contenido HTML con una URL base o un elemento de su historial. Sus métodos de carga devuelven una secuencia asíncrona de eventos de navegación y están marcados con @discardableResult.
Eso permite ignorar el resultado en una navegación sencilla o consumirlo cuando necesitamos registrar el proceso, coordinar estados adicionales o diagnosticar un fallo:
func openGuide(on page: WebPage) async {
let url = URL(string: "https://example.com/guides/getting-started")!
do {
for try await event in page.load(url) {
print("Navegación: \(event)")
}
} catch {
print("No se pudo cargar la guía: \(error)")
}
}
La ejecución de JavaScript también encaja en el modelo moderno de concurrencia. callJavaScript es una operación async que devuelve su resultado sin recurrir a un bloque de finalización:
let heading = try await page.callJavaScript(
"return document.querySelector('h1')?.textContent"
)
print(heading)
Que la API sea más cómoda no cambia su frontera de seguridad. El código JavaScript debe tratarse como una interacción con contenido potencialmente externo, especialmente si la URL no está controlada por la aplicación. Tampoco conviene convertir un WebView en un navegador general sin decidir antes cómo gestionar enlaces externos, descargas, permisos y esquemas distintos de HTTP y HTTPS.
Modificadores específicos para contenido web
La nueva vista incluye modificadores que antes exigían configurar propiedades de WKWebView o de sus vistas internas:
WebView(page)
.webViewContentBackground(.hidden)
.webViewMagnificationGestures(.enabled)
.webViewLinkPreviews(.disabled)
.webViewTextSelection(.enabled)
.findNavigator(isPresented: $isSearching)
Ocultar el fondo del contenido resulta especialmente útil para integrarlo con el fondo de la pantalla y evitar el destello blanco que podía aparecer durante la carga. El resto de modificadores controla el gesto de ampliación, las previsualizaciones al mantener pulsado un enlace, la selección de texto y la interfaz del sistema para buscar dentro de la página.
Convivir con versiones anteriores
WebView y WebPage requieren iOS 26. Si la aplicación mantiene iOS 17 como versión mínima, el wrapper de WKWebView seguirá siendo necesario durante la transición. Conviene aislar la implementación moderna en una vista marcada con disponibilidad, porque ni siquiera podemos declarar una propiedad WebPage dentro de un tipo que deba existir en sistemas anteriores.
struct WebContentView: View {
let url: URL
@ViewBuilder
var body: some View {
if #available(iOS 26.0, *) {
ModernWebContentView(url: url)
} else {
LegacyWebView(url: url)
}
}
}
@available(iOS 26.0, *)
private struct ModernWebContentView: View {
@State private var page = WebPage()
let url: URL
var body: some View {
WebView(page)
.task {
page.load(url)
}
}
}
Esta separación facilita retirar LegacyWebView cuando aumente la versión mínima sin contaminar el resto de la interfaz con comprobaciones de disponibilidad.
Una integración más propia de SwiftUI
La mejora no consiste únicamente en escribir menos líneas. WebView se ocupa de representar el contenido y WebPage concentra el estado y las operaciones, una división que encaja mucho mejor con la arquitectura de SwiftUI. Observation mantiene sincronizados título, progreso e historial, mientras que AsyncSequence y async/await sustituyen buena parte de la coordinación basada en delegados y bloques de finalización.
WKWebView continúa siendo la base de WebKit y sigue teniendo sentido en implementaciones heredadas o muy personalizadas. Sin embargo, para la mayoría de pantallas que muestran contenido web, iOS 26 convierte una integración tradicionalmente imperativa en una pieza natural de una jerarquía SwiftUI.