Deploying Native Apps

In order to publish a PWA as a stand alone native application in the App Store it’s required to add extra functionality which is not available by opening PWA in a web browser. Allowing to receive push notifications, scan QR codes, and access user contacts might be such features.

To achieve this goal it’s required to create a native iOS app containing WKWebView instance (references as webView in the following example code) and WKScriptMessageHandler protocol implementation (JSMessageHandler in example). PWA should provide a UI and send messages using Web Kit API. This API is injected by WKWebView itself and is accessed by calling a function in a following manner:

window.webkit.messageHandlers.<message_name>.postMessage("Hello, native world!");

Example project is configured to respond to following messages: exampleMessage, exampleMessageWithArgument

To invoke them in the example project it’s required to make following function calls on PWA side:

window.webkit.messageHandlers.exampleMessage.postMessage();
window.webkit.messageHandlers.exampleMessageWithArgument.postMessage("Hello, native world!");

Returning to the native code of the example project let’s take a look at the JSMessageHandler class. It consists of the two parts. First one is implementation details and it declares constants with message names and declares two closure properties which should be called when a message from the PWA side is received.

Second part is the userContentController function and this is the WKScriptMessageHandler protocol function. It’s called when a message to Web Kit API is sent from the web side. WKScriptMessage is passed as an argument and this object contains the actual message name and arguments passed to it. It’s a WKScriptMessageHandler responsibility to identify the message and convert the argument passed if needed. In the following example it’s achieved by using a switch operator and calling a corresponding closure if message is identified.

class JSMessageHandler: NSObject, WKScriptMessageHandler {
    struct MessageNames {
        static let exampleMessage = "exampleMessage"
        static let exampleMessageWithArgument = "exampleMessageWithArgument"
    }
    var exampleAction: (() -> Void)?
    var exampleActionWithArgument:((String) -> Void)?
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
        case MessageNames.exampleMessage:
            exampleAction?()
        case MessageNames.exampleMessageWithArgument:
            if let argument = message.body as? String {
                exampleActionWithArgument?(argument)
            } else {
                print("Wrong argument type for message \(message.name) : \(message.body)")
            }
        default:
            print("Unknown message: \(message.name)")
        }
}

Second class of the example project is a ViewController class which creates a JSMessageHandler instance and configures WKWebView instance with it. Following function creates WKWebViewConfiguration instance:

func createWebViewConfiguration() -> WKWebViewConfiguration {
        let contentController = WKUserContentController()
        let handler = JSMessageHandler()
        handler.exampleAction = { [weak self] in
            print("exampleMessage was sent from PWA and is handled in native code")
            self?.webView.evaluateJavaScript("window.exampleActionCallback()", completionHandler: nil)
        }
        handler.exampleActionWithArgument = { [weak self] argument in
            print("exampleMessageWithArgument '\(argument)' was sent from PWA and is handled in native code")
            self?.webView.evaluateJavaScript("window.exampleActionWithArgumentCallback('\(argument + " Hello from native code!")')", completionHandler: nil)
        }
        contentController.add(handler, name: JSMessageHandler.MessageNames.exampleMessage)
        contentController.add(handler, name: JSMessageHandler.MessageNames.exampleMessageWithArgument)
        let config = WKWebViewConfiguration()
        config.userContentController = contentController
        return config
    }

First of all there is a WKUserContentController instance created. Afterwards a JSMessageHandler instance is created and its actions are populated. In this example these actions just report to the console and then call window.exampleActionCallback() or window.exampleActionWithArgumentCallback() correspondingly. It’s suggested that these functions are added by PWA and it will process the calls to them. In real world project this process most likely will be asynchronous and native code will call back to PWA after user interaction (getting QR code, accessing contacts etc)

Afterwards the content controller is instructed to pass the message handling for each expected message to create a JSMessageHandler instance. WKWebViewConfiguration is created, configured and returned afterwards.

lazy var webView: WKWebView = {
        let wv = WKWebView(frame: .zero, configuration: createWebViewConfiguration())

WKWebView instance is created as follows and should open PWA by URL later. The rest of the code is a standard UIKit / WebKit and is skipped for clarity. See attached example project. It’s configured to open a non-existing "https://your.pwa.com" site. You can change it to your own PWA URL.

Last updated