Programación en Swift y SwiftUI para iOS Developers

Como usar WebView y WebPage en SwiftUI

Durante mucho tiempo, cuando desarrollábamos apps con SwiftUI queríamos mostrar contenido web dentro de nuestra app —por ejemplo, una página de ayuda, un mini navegador, un híbrido web-nativo— y la única vía era recurrir a la integración de WKWebView vía UIKit (usando UIViewRepresentable) o usar SFSafariViewController. Esto implicaba bastante “boilerplate” y cierta complejidad.
Con iOS 26, Apple ha introducido una vista web nativa de SwiftUI, la clase WebView (y el modelo de navegación asociado WebPage) para que podamos integrar contenido web de forma más directa, declarativa y “SwiftUI-native”. AppCoda+2Differ+2
Este tutorial te guiará por:

  1. Cómo usar WebView básica.
  2. Cómo usar WebPage para un control más fino.
  3. Cómo manejar navegación, JavaScript, html local.
  4. Buenas prácticas, precauciones y casos reales.

Voy a asumir que tienes conocimientos básicos de SwiftUI.


1. Uso básico de WebView

Lo primero: para cargar una página web simple con SwiftUI en iOS 26 podemos hacer algo así:

import SwiftUI
import WebKit

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


Eso significa que ya no es necesario envolver manualmente un WKWebView mediante UIViewRepresentable.

Plantilla mínima

struct MyWebView: View {
    let url: URL

    var body: some View {
        WebView(url: url)
    }
}

Y luego usar:

MyWebView(url: URL(string: "https://my-site.com")!)

Ventajas iniciales

  • Código más limpio, menos “puente” entre UIKit y SwiftUI.
  • Mantienes el motor WebKit que Safari usa internamente.
  • Menos errores y menos boilerplate para algo que era muy común.
  • Compatible con SwiftUI declarativo.

Precaución

  • Asegúrate de que la URL sea válida o maneja la opción de nil.
  • Carga de URLs remotas exige permisos de red/configuración de dominio si es necesario (ATS de Apple).
  • Aunque es “nativo”, aún internamente usa WebKit y hereda ciertas consideraciones de rendimiento y seguridad.

2. Uso de WebPage para control avanzado

Para casos donde necesitas más control —ejemplo: obtener título de la página, progreso de carga, recargar, ejecutar JavaScript, gestionar navegación— iOS 26 también propone el tipo WebPage. Veamos un ejemplo:

import SwiftUI
import WebKit

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

    var body: some View {
        VStack {
            WebView(page)
                .onAppear {
                    if let url = URL(string: "https://www.apple.com") {
                        let request = URLRequest(url: url)
                        page.load(request)
                    }
                }
            Text("Título: \(page.title ?? “-”)")
            Text("URL actual: \(String(describing: page.url))")
            Text("Progreso: \(page.estimatedProgress.formatted(.percent.precision(.fractionLength(0))))")
            HStack {
                Button("Recargar") { page.reload() }
                Button("Detener") { page.stopLoading() }
            }
        }
    }
}

Explicación de lo que hace

  • WebPage() crea un objeto que representa el “estado” de una página web.
  • WebView(page) conecta la vista a ese objeto.
  • Puedes acceder a titleurlestimatedProgress para mostrar datos al usuario. AppCoda
  • Puedes llamar load(_:)reload()stopLoading() para control.
  • También puedes cargar HTML directamente:
page.load(html: "<h1>Hola desde SwiftUI</h1>", baseURL: URL(string:"about:blank")!)
``` :contentReference[oaicite:10]{index=10}

Cuándo usar WebPage en lugar de “solo URL”

  • Si necesitas que el usuario vea el título de la página o el progreso de carga.
  • Si quieres implementar botones “Atrás/Adelante”.
  • Si necesitas ejecutar JavaScript personalizado o interceptar navegación.
  • Si estás construyendo un mini-navegador o un componente web más complejo, no solo “mostrar web”.

3. Ejemplos prácticos

Ejemplo A: Mini navegador simple

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

    var body: some View {
        VStack {
            HStack {
                Button(action: { Task { try? await page.callJavaScript("history.back()") } }) {
                    Image(systemName: "chevron.backward")
                }
                Button(action: { page.reload() }) {
                    Image(systemName: "arrow.clockwise")
                }
                Spacer()
                if page.isLoading {
                    ProgressView()
                }
            }.padding()

            WebView(page)
                .ignoresSafeArea(.bottom)

        }
        .onAppear {
            if let url = URL(string: "https://www.apple.com") {
                page.load(URLRequest(url: url))
            }
        }
    }
}

Detalles:

  • Usamos callJavaScript para ejecutar script (en este caso “history.back()”).
  • Tratamos los botones manualmente.
  • Agregamos ProgressView() para indicar que está cargando.
  • ignoresSafeArea(.bottom) para que ocupe toda la pantalla.

Ejemplo B: Cargar HTML local

struct LocalHtmlView: View {
    @State private var page = WebPage()
    let htmlContent = """
        <html><body><h1>Bienvenido</h1><p>Este es HTML local en la app.</p></body></html>
        """

    var body: some View {
        WebView(page)
            .onAppear {
                page.load(html: htmlContent, baseURL: URL(string: "about:blank")!)
            }
    }
}

Esto es útil para mostrar contenido de ayuda, documentación offline, plantillas que no requieren red.

Ejemplo C: Navegación externa / enlace personalizado

Si el sitio web dentro del WebView contiene enlaces que quieres interceptar o manejar externamente, puedes usar WebPagepara observación. Por ejemplo:

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

    var body: some View {
        WebView(page)
            .onChange(of: page.url) { newURL in
                if let u = newURL, u.host == "external.example.com" {
                    // abrir en Safari u otra vista
                    UIApplication.shared.open(u)
                    page.stopLoading()
                }
            }
            .onAppear {
                if let url = URL(string: "https://your-site.com") {
                    page.load(URLRequest(url: url))
                }
            }
    }
}

Esta técnica permite interceptar ciertos redireccionamientos o enlaces “externos”.


4. Buenas prácticas y consideraciones

Rendimiento

  • Evita cargar demasiadas webs con scripts pesados sin necesidad.
  • Si solo necesitas mostrar contenido estático, usa HTML local.
  • Considera caché, política de cookies, almacenamiento local (esto ya lo maneja WebKit pero tenlo en mente).
  • Consulta que el WebView no esté bloqueando la interfaz principal — aunque SwiftUI ya gestiona bien la UI.

Seguridad

  • Asegúrate de que las URLs que cargas sean de confianza, o forzar HTTPS (App Transport Security).
  • Si ejecutas JavaScript, evalúa posibles vulnerabilidades de inyección.
  • Si la web carga contenido mixto o de terceros, asegúrate de las políticas de privacidad.

Manejo de enlaces “Atrás/Adelante”

  • A diferencia de WKWebView tradicional, la nueva WebView/ WebPage no tienen por defecto botones de navegación. Debes implementarlos (o ejecutar JavaScript para “history.back()”).
  • Algunos usuarios reportan que el retroceso hacia atrás no siempre es intuitivo. Reddit
  • Si requieres navegación compleja, asegúrate de dar al usuario botones visibles.

Compatibilidad y fallback

Aunque iOS 26 tiene WebView nativo, quizá debas dar soporte a versiones anteriores de iOS (por ejemplo iOS 15, 16). En ese caso, puedes tener una implementación condicional:

if #available(iOS 26, *) {
    WebView(url: myURL)
} else {
    LegacyWebView(url: myURL)
}

Donde LegacyWebView envuelve WKWebView con UIViewRepresentable. Documentado desde hace años. Medium+1

UI/UX

  • Puedes añadir ProgressView() para indicar carga.
  • Considera usar navigationTitle(page.title ?? "") si usas NavigationStack.
  • Maneja estados de error (pagina no carga / URL inválida) presentando un texto de “Lo sentimos, no se pudo cargar.”
  • Usa ignoresSafeArea() si quieres ocupar todo el espacio.
  • Testea en modelos de iPhone e iPad.

Plugins o frameworks externos

Si por alguna razón necesitas aún más control, existen bibliotecas como WebViewKit que envuelven el web view para SwiftUI con extensiones. Pero si estás en iOS 26 lo ideal es usar la solución nativa.


5. Caso de estudio completo

Imaginemos que estamos construyendo una app que tiene una sección “Ayuda” que carga una página web, y queremos: título, retroceso, recarga, indicador de carga, y manejo de enlaces externos.

import SwiftUI
import WebKit

struct HelpWebView: View {
    @State private var page = WebPage()
    @State private var showExternalLinkAlert = false
    @State private var externalURL: URL?

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                HStack {
                    Button(action: { Task { try? await page.callJavaScript("history.back()") } }) {
                        Image(systemName: "chevron.backward")
                    }.disabled(page.canGoBack == false)

                    Button(action: { page.reload() }) {
                        Image(systemName: "arrow.clockwise")
                    }

                    Spacer()

                    if page.isLoading {
                        ProgressView()
                    }
                }.padding()

                WebView(page)
                    .onChange(of: page.url) { newURL in
                        guard let u = newURL else { return }
                        if u.host == "external.myhelp.com" {
                            externalURL = u
                            showExternalLinkAlert = true
                        }
                    }
            }
            .navigationTitle(page.title ?? "Ayuda")
            .navigationBarTitleDisplayMode(.inline)
            .onAppear {
                if let url = URL(string: "https://help.myapp.com") {
                    page.load(URLRequest(url: url))
                }
            }
            .alert("Ir al enlace externo?", isPresented: $showExternalLinkAlert) {
                Button("", role: .destructive) {
                    if let u = externalURL {
                        UIApplication.shared.open(u)
                    }
                }
                Button("No", role: .cancel) {
                    externalURL = nil
                }
            } message: {
                Text(externalURL?.absoluteString ?? "")
            }
        }
    }
}

Comentarios

  • page.canGoBack se asume que existe (revisar API real) o puedes mantener un @State boolean actualizado mediante Combine si la clase lo expone.
  • Creamos un NavigationStack para mostrar título.
  • Detectamos cambios de URL para interceptar enlaces externos.
  • Usamos un alert para confirmación.
  • Indicador de carga, botón recargar, botón atrás.

Este tipo de componente puede reutilizarse para diversas secciones web de tu app.

6. Diferencias respecto al “viejo” enfoque con UIViewRepresentable

Antes de iOS 26, debíamos hacer algo así:

struct LegacyWebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.load(URLRequest(url: url))
    }
}

Y luego usarlo en SwiftUI. Esto tenía desventajas: delegado de navegación, boilerplate, difícil de combinar con SwiftUI puro. 

Con la nueva WebView de SwiftUI, hace que tu código sea más limpio, más declarativo y mejor mantenible. 


7. Limitaciones actuales y cosas a revisar

  • Algunos desarrolladores han reportado “bugs” o comportamientos inesperados al usar .ignoresSafeArea() junto con la nueva WebView en iOS 26. Reddit
  • La capacidad de navegación hacia atrás/adelante (goBack/goForward) aún no tan directa como el WKWebView, puede requerir ejecutar JavaScript manualmente. Reddit
  • Si necesitas soporte para iOS < 26, deberás mantener el enfoque heredado como fallback.
  • Si necesitas personalización avanzada de configuración de WKWebView (por ejemplo: WKWebViewConfigurationWKUserScriptcustomUserAgent), quizá aún debas usar el enfoque clásico o un wrapper que exponga esta personalización.
  • Performance: aunque está optimizado, cargar muchas webs complejas puede generar consumo de memoria o sobrecarga de procesos WebKit — especialmente en dispositivos más antiguos.

8. Conclusión

Con la llegada de iOS 26, integrar contenido web en apps SwiftUI ha dejado de ser “un engorro” y se convierte en algo que podemos hacer con casi la misma facilidad con la que mostramos un Image o un Text.
El nuevo componente WebView y el modelo WebPage hacen que el proceso sea más fluido, limpio y mantenible.
Si solo necesitas mostrar una web simple: ✨ usa WebView(url:).
Si necesitas control, estadísticas, navegación, o ejecutar JavaScript: usa WebPage() + WebView(page).
Y no olvides: considera el rendimiento, la seguridad, la compatibilidad hacia atrás, y prueba bien en diferentes dispositivos.

Con estos ejemplos y buenas prácticas, ya estás preparado para incorporar vistas web nativas en tu app iOS 26 basada en SwiftUI.

Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Como crear un TextField multilinea en SwiftUI

Next Article

Como crear una Splash Screen en SwiftUI

Related Posts