DEV Community

Cover image for Over-the-air updates for WKWebView content
Teodor Dermendzhiev for ChattyWebviews

Posted on • Updated on

Over-the-air updates for WKWebView content

ChattyWebviews is a simple tool based on Firebase to help you get more from your webviews

Webviews enable developers to reuse code across different platforms, leading to significant time and resource savings. Major companies like Netflix have successfully leveraged webviews in their apps, using web technologies to deliver consistent user experiences across iOS and Android.

Netflix, for instance, initially used webviews to rapidly experiment with and iterate on their user interface. As Jordanna Kwok, Senior Software Engineer at Netflix, explains in an interview with Hacking with Swift:
"Webviews allowed us to A/B test large changes and get almost immediate feedback."

This ability to move fast and adapt quickly is especially crucial for startups and new product development, where the landscape is constantly changing and speed to market can be a key differentiator.

One of the key advantages of webviews is the ability to update the JavaScript code remotely, allowing for real-time updates to the webview content. This is where Over-the-Air (OTA) updates come into play, enabling developers to push updates directly to the webview components of an app, bypassing the traditional app store approval process.

In this blog post, I'll introduce you to ChattyWebviews, a simple framework that bridges the gap between web and native environments. ChattyWebviews facilitates seamless communication between webviews and native classes, making it an good solution for developers aiming to maintain agility in their app updates and promote UI reusability across platforms.

ChattyWebviews is not an alternative to native development or any cross-platform framework. It can be used alsongside any of them.

I'll guide you through the process of implementing OTA updates for the webview components of a standard iOS app using ChattyWebviews. We'll create a simple app with a login screen living in a webview. This login screen will not only be reusable between iOS and Android but also updatable using ChattyWebviews' OTA update feature.

So, let's dive in and explore how you can enhance your cross-platform app development process with Chatty Webviews and OTA updates.

We'll be using a simple app called CWLoginDemo for this purpose. You can create this app from scratch, or you can clone the project directly from here.

The CWLoginDemo app consists of a home screen with a login button. When the login button is tapped, a modal screen is presented, displaying the login page. This login page is not a typical iOS view, but an Angular app running inside a webview.

The login page we're using is a modified version of a project by Jason Watmore. When the user enters their username and password into the fields on the login page, the native class will receive the message, process the login, and close the modal.
Let's get into the details.

Setting up the CWLoginDemo App

Start by creating a new iOS app project and name it CWLoginDemo. Alternatively, you can clone the project directly from here. The app should consist of a home screen with a login button.

import SwiftUI
import ChattyWebviews

struct ContentView: View {
    @State private var showingSheet = false


    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Button("Login") {
                        showingSheet.toggle()
                    }
                    .sheet(isPresented: $showingSheet) {
                        SheetView()
                    }
        }
        .padding()
    }

    init(showingSheet: Bool = false) {
        self.showingSheet = showingSheet
        self.setupXPModules()
    }

    //1
    func setupXPModules() {
        let module = CWModule(name: "demo", location: .Resources)
        WebViewFactory.initModules(modules: [module])
    }
}

struct SheetView: UIViewControllerRepresentable, CWMessageDelegate {

    typealias UIViewControllerType = CWViewController

    @Environment(\.dismiss) var dismiss

    //3
    func controller(_ controller: ChattyWebviews.CWViewController, didReceive message: ChattyWebviews.CWMessage) {
        print(message)
        dismiss()

    }

    //2
    func makeUIViewController(context: Context) -> ChattyWebviews.CWViewController {
        let module = WebViewFactory.getModules()[0]
        let scheduleVC = WebViewFactory.createWebview(from: module, path: nil)
        scheduleVC?.messageDelegate = self
        return scheduleVC! 
    }

    func updateUIViewController(_ uiViewController: ChattyWebviews.CWViewController, context: Context) {

    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. First we need to initialize the web module with the name of our angular project contents (inside it's dist folder).

  2. The makeUIViewController method returns a UIViewController, so we create the container of our webview app using WebViewFactory.createWebview. Remember to be careful with force unwrapping.

  3. Above we set the messageDelegate property to self so the SheetView struct receives messages from the login webview. Here we just print the message and dismiss it.

Of course, for this to work you will have to install ChattyWebviews SDK. For the sake of this tutorial I used SPM, but you can also use Cocoapods or copy-paste the source code.

Integrating the Webview Login Page

The login page is an Angular app that we'll run inside a webview. When the login button on the home screen is tapped, a modal screen with the webview login page should be presented.

First, install the chatty-webviews-js package:

npm install @chatty-webviews/js --save
Enter fullscreen mode Exit fullscreen mode

When you have it installed, don't forget to call attach() method:

import { Component } from '@angular/core';
import { attach } from '@chatty-webviews/js';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {

    constructor() {
        attach();  
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in login.component.ts I replaced the this.accountService.login call with sendMessage(). Here is the entire component:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { sendMessage } from '@chatty-webviews/js';

import { AlertService } from '../_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
    form!: FormGroup;
    loading = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            username: ['', Validators.required],
            password: ['', Validators.required]
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.loading = true;
        sendMessage("login-successful", {username: "Teodor"});
    }

}
Enter fullscreen mode Exit fullscreen mode

When the user enters their username and password into the fields on the login page, the native class will receive the message. This message should trigger the processing of the login and the closing of the modal.

Now build the project:

npm run build
Enter fullscreen mode Exit fullscreen mode

When the build completes drag the project folder from inside the dist directory to your Xcode project. It's important to select "Create folder references". If you leave "Copy items if needed" unchecked you won't need to drag the package folder after every build, but it won't be in the Xcode project repository, so decide based on your project structure.

Image description

Now, if you run the app and tap on the Login button you should see this:

Image description

Setting Up Your Environment for Over-the-Air Updates

Before we dive into the process of implementing over-the-air updates, we need to set up our environment. This involves creating a Firebase project and deploying the backend function to Firebase. Here's how to do it:

Step 1: Create a Firebase Project

First, you'll need to create a Firebase project.

Image description

As part of this process, make sure to enable authentication with email/password, storage, and the Firestore database. Please note that you'll need to have billing enabled to deploy the functions.

Step 2: Clone the Backend Repository

Next, clone the backend repository from here. Once you've cloned the repository, run npm install to install the necessary dependencies.

Step 3: Install Firebase Tools

If you haven't installed Firebase tools yet, you can do so with the following command:

npm install -g firebase-tools
Enter fullscreen mode Exit fullscreen mode

Then, log in to Firebase using the command:

firebase login
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy Functions to Firebase

Now, it's time to deploy the functions to Firebase. You can do this with the following command:

firebase deploy - only functions - project <your-project-id>
Enter fullscreen mode Exit fullscreen mode

You can find your project ID by running:

firebase projects:list
Enter fullscreen mode Exit fullscreen mode

If you encounter an error that says "Error: There was an error deploying functions" it's likely related to your Node.js version. Ensure you're using Node.js 16 (or the same version as specified in the functions' package.json):

"engines": {
 "node": "16"
}
Enter fullscreen mode Exit fullscreen mode

After you've deployed the function successfully, copy it's url from the function's dashboard:

Image description

With these steps, you've successfully set up your environment for over-the-air updates. In the next section, we'll dive into how to implement these updates in your iOS app.

Pushing the updates

Now that we have our environment set up, we can proceed to implement over-the-air updates. This process involves using the ChattyWebviews CLI and setting up some environment variables on your machine.

Step 1: Install ChattyWebviews CLI

The ChattyWebviews CLI is a command-line interface that simplifies the process of managing your ChattyWebviews projects. To install it, run the following command:

npm install -g @chatty-webviews/cli
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up Environment Variables

Next, you need to set up some environment variables on your machine. These variables will store the configuration details of your Firebase project. Here's how to do it:

On a Unix-based system like Linux or macOS, you can set environment variables using the export command:

export FIREBASE_API_KEY="<your_api_key>"
export FIREBASE_AUTH_DOMAIN="<your_auth_domain>"
export FIREBASE_PROJECT_ID="<your_project_id>"
export FIREBASE_STORAGE_BUCKET="<your_storage_bucket>"
export FIREBASE_APP_ID="<your_app_id>"
Enter fullscreen mode Exit fullscreen mode

On Windows, you can use the set command:

set FIREBASE_API_KEY="<your_api_key>"
set FIREBASE_AUTH_DOMAIN="<your_auth_domain>"
set FIREBASE_PROJECT_ID="<your_project_id>"
set FIREBASE_STORAGE_BUCKET="<your_storage_bucket>"
set FIREBASE_APP_ID="<your_app_id>"
Enter fullscreen mode Exit fullscreen mode

These commands set the environment variables for the current session only. If you want to set them permanently, you'll need to add these lines to your shell's startup file (.bashrc, .bash_profile, or .zshrc for Unix-based systems, or Environment Variables on Windows).

Replace <your_api_key>, <your_auth_domain>, <your_project_id>, <your_storage_bucket>, and <your_app_id> with your actual Firebase project details. You can get them by creating a web project from the Firebase console:

Image description

With the ChattyWebviews CLI installed and your environment variables set, you're now ready to push over-the-air updates to your app. First, tell ChattyWebviews more about your project by calling this command:

chatty init
Enter fullscreen mode Exit fullscreen mode

It will ask you for email, password and the path to your package directory - in our case I pasted the absolute path to dist/demo.

Now go to login.component.ts and update something in the UI - e.g. replace the login button title with capital letters.

<button [disabled]="loading" class="btn btn-primary">
   <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
   LOGIN
</button>
Enter fullscreen mode Exit fullscreen mode

Build the package:

npm run build
Enter fullscreen mode Exit fullscreen mode

And push the update to Firebase:

chatty ci release --version 1.0.1
Enter fullscreen mode Exit fullscreen mode

Before testing the update you need to add few things to the setupXPModules:

func setupXPModules() {
        ModuleSynchronizer.updateCheckUrl = "<your-checkForUpdate-url>"

        let module = CWModule(name: "demo", currentHash: "", location: .Resources)
        WebViewFactory.initModules(modules: [module])
        ModuleSynchronizer.shared.updateIfNeeded(module: module, email: "tester@test.com", appId: "cw-login-demo") { success in
            print("Module \(module.name) updated! Please restart the app.")
        }
    }
Enter fullscreen mode Exit fullscreen mode

When you run the app the login screen will be updated.

Image description

Top comments (1)

Collapse
 
vmutafov profile image
Vladimir Mutafov

Nicely done!