Si eres desarrollador móvil, tarde o temprano tendrás que trabajar con webviews en tu aplicación. Lo habitual es utilizarlos para mostrar simples páginas web. Sin embargo, ¿sabías que puedes intercambiar información con ellos para ofrecer una experiencia híbrida a tus usuarios?

Comunicación nativa-web

Hace ya muchos años que Apple recomendó dejar de utilizar el navegador embebido proporcionado por el framework UIKit, el famoso UIWebView. En su lugar, a partir de iOS 8 se comenzó a utilizar WKWebView como alternativa que podemos encontrar dentro del framework WebKit de Apple.

Una de las ventajas que tiene este componente es que permite crear un canal de comunicación entre la app y el propio navegador. Esto es muy útil en aplicaciones que, en ciertas partes de la misma, necesitan contenido fácilmente actualizable, es decir, sin tener que recurrir a generar una nueva versión de la app y tener que volver subir a la store.

Para conseguir esto, Apple nos permite configurar nuestro WKWebView con un objeto que se encarga de gestionar este canal de comunicación. Este componente es WKUserContentController

WKUserContentController

Según la documentación de Apple vemos que este componente nos permite:

Usarlo es tan sencillo como inicializar nuestro webview con una configuración determinada:

let userContentController = WKUserContentController()
userContentController.add(self, name: "myNativeApp")

let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.userContentController = userContentController

let webView = WKWebView(frame: view.bounds, configuration: webViewConfiguration)
view.addSubview(webView)

Como habrás podido observar, a la hora de inicializar nuestro WKUserContentController le hemos añadido self como “message handler” y lo hemos llamado “myNativeApp”. Este será nuestro canal de comunicación por el cual JavaScript podrá comunicarse con nuestra app:

window.webkit.messageHandlers.myNativeApp.postMessage(message);

Nota: este método solo funciona dentro del WKWebView, por lo que en un navegador externo no se podrá usar.

Recibir información en Swift desde JavaScript

Ahora que conocemos las virtudes de WKUserContentController, imaginemos que tenemos un formulario web en un archivo HTML dentro de nuestra app dónde se pide nuestro nombre y email:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
        <script type="text/javascript">
            function submitForm() {
                var message = {
                    name: document.getElementById("name").value,
                    email: document.getElementById("email").value
                };
                window.webkit.messageHandlers.myNativeApp.postMessage(message);
            };
        </script>
    </head>
    <body>
    <div>
        <label id="message">Rellena el siguiente formulario:</label>
        <div>
            <label for="name">Nombre:</label>
            <input type="text" id="name" placeholder="Tu nombre" name="name">
        </div>
        <div>
            <label for="email">Email:</label>
            <input type="email" id="email" placeholder="Tu email" name="email">
        </div>
        <button onclick="submitForm()">Enviar</button>
        </div>
    </body>
</html>

En este formulario, al pulsar en “Enviar” se llama a la función submitForm donde se crea un diccionario con el nombre y el email y se le pasa a la app a través del canal “myNativeApp” que hemos creado.

Por su parte, nuestra app es capaz de recibir esa información implementando el siguiente método:

class ViewController: UIViewController {

    private lazy var webViewConfiguration: WKWebViewConfiguration = {
        let userContentController = WKUserContentController()
        userContentController.add(self, name: "myNativeApp")
        let webViewConfiguration = WKWebViewConfiguration()
        webViewConfiguration.userContentController = userContentController
        return webViewConfiguration
    }()

    private lazy var webView: WKWebView = {
        return WKWebView(frame: view.bounds, configuration: webViewConfiguration)
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
        loadHTML()
    }
}

private extension ViewController {

    func setupWebView() {
        view.addSubview(webView)
    }

    func loadHTML() {
        guard let url = Bundle.main.url(forResource: "MyPage", withExtension: "html") else { return }
        webView.load(URLRequest(url: url))
    }
}

extension ViewController: WKScriptMessageHandler {

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let formValues = message.body as? [String: AnyObject] else { return }
        print(formValues)
    }
}

Aquí es nuestro ViewController quien actúa como WKScriptMessageHandler al que hemos llamado “myNativeApp”, que recibe la información a través del método didReceiveMessage.

Si rellenamos el formulario y pulsamos “Enviar” veremos en la consola de Xcode el diccionario con nuestros datos:

["email": bob@mail.com, "name": Bob]

Enviar información desde Swift a JavaScript

Al igual que JavaScript puede comunicarse con la aplicación, nuestro código Swift también puede interactuar con el contenido del navegador.

En nuestro ejemplo vamos a hacer uso de los datos obtenidos del formulario para actualizar el título del mismo. Para ello añadimos una nueva función al código JavaScript:

<script type="text/javascript">

    function submitForm() {
        var message = {
            name: document.getElementById("name").value,
            email: document.getElementById("email").value
        };         window.webkit.messageHandlers.myNativeApp.postMessage(message);
    };

    function updateFormMessage(json) {
        document.getElementById("message").innerHTML = "Tu nombre es "+json["name"]+" y tu email "+json["email"];
    };
</script>

La nueva función updateFormMessage recibe un json que se utiliza para cambiar el título del formulario.

Por su parte, en la app:

private extension ViewController {
    
    func setupWebView() {
        view.addSubview(webView)
    }
    
    func loadHTML() {
        guard let url = Bundle.main.url(forResource: "MyPage", withExtension: "html") else { return }
        webView.load(URLRequest(url: url))
    }
    
    func updateFormMessage(values: [String: AnyObject]) {
        guard let jsonData = try? JSONSerialization.data(withJSONObject: values),
              let jsonString = String(data: jsonData, encoding: .utf8) else { return }
        webView.evaluateJavaScript("updateFormMessage(\(jsonString))")
    }
}

extension ViewController: WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let formValues = message.body as? [String: AnyObject] else { return }
        updateFormMessage(values: formValues)
    }
}

El nuevo método updateFormMessage recoge el diccionario obtenido del formulario, lo convierte a una cadena JSON y se le proporciona a la función correspondiente de JavaScript haciendo uso del método evaluateJavaScript del webview.

Método evaluateJavaScript del webview.

Inyección de código JavaScript

Seguramente te estarás preguntando por qué hemos usado el evaluateJavaScript del propio navegador para pasar información a este en vez de hacer uso de WKUserContentController.

WKUserContentController permite inyectar código JavaScript mediante WKUserScripts:

let js = "updateFormMessage(\(jsonString))"

let userScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

userContentController.addUserScript(userScript)

Sin embargo, este código es ejecutado únicamente al iniciar o al finalizar la carga de la web, a diferencia de evaluateJavaScript que inyecta el código “en caliente”.

Además, ambos métodos tienen diferentes privilegios a la hora de ejecutar código por lo que hay que estudiar en cada caso cuál conviene utilizar. Podemos encontrar más información aquí.

Conclusiones

Por mucho que nos guste desarrollar funcionalidades nativas en nuestras aplicaciones iOS, hay momentos en los que es necesario proporcionar una solución híbrida que ofrezca un equilibrio entre experiencia de usuario y flexibilidad de despliegue. Gracias a WKWebView y WKUserContentController podemos desarrollar un sistema de comunicación entre funcionalidades web y la propia aplicación, todo ello garantizando la seguridad y versatilidad necesaria en nuestros desarrollos.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.