EasyApp

Native and H5 Interaction

Learn how to interact with H5 in your app

EasyApp includes a built-in WebView component that makes it easy to interact with H5.

Basic Usage

1. Load a remote webpage

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

2. Load an offline webpage

struct LocalWeb: View {

    @StateObject var viewModel = LocalWebViewModel()

    var body: some View {
        WebViewReader { proxy in
            VStack {
                WebView(configuration: viewModel.configuration)
            }
            .onAppear {
                proxy.loadHTMLString(viewModel.htmlString, baseURL: viewModel.htmlURL)
            }
        }

    }
}

class LocalWebViewModel: CommonEAWebViewModel {

    let htmlURL = Bundle.main.url(forResource: "sample", withExtension: "html")!
    let htmlString: String

    override init() {
        htmlString = try! String(contentsOf: htmlURL, encoding: .utf8)
        super.init()
    }
}

You need to add the sample.html file to your project.

For a fully featured offline H5 experience, you may need incremental updates and server-delivered H5 content. This is beyond the scope of this guide—please research as needed.

Native ↔ H5 Interaction

1. How can H5 call native capabilities?

We provide an example here: EasyApp-WebView-Debugger

The example demonstrates:

  • Calling native Alert
  • Calling native Confirm
  • Calling native Alert Input
  • Initiating a phone call
  • Initiating an SMS
  • Initiating FaceTime
  • Sending an email
  • And more that you can implement yourself in the example project
  • Selecting files/photos
  • Analyzing photos
  • Getting current location
  • Accessing camera/microphone

Note: EasyApp does not include the following permissions by default. If your app does not use these capabilities, per Apple’s review rules, you should not request them. To keep review simple, EasyApp omits them by default.

If you need camera, microphone, or location from H5, add the following permissions to your Info.plist:

<key>NSCameraUsageDescription</key>
<string>We need access to your camera to let you share them with your friends.</string>

Microphone

<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone to let you share them with your friends.</string>

Location

<key>NSLocationWhenInUseUsageDescription</key>
<string>We need access to your location to let you share them with your friends.</string>

As shown below: privateAuth.png

2. How does native inject scripts?

See ScriptMessageHandler.swift for an example:

EasyAppSwiftUI/App/Developer/SubPages/OnlineWeb/ViewModel/ScriptMessageHandler.swift
    /// Setup WebView JavaScript bridge
    private func setupWebViewBridge() {
        let source = """
            injectYourScriptHere
            """
        
        let userScript = WKUserScript(
            source: source, 
            injectionTime: .atDocumentStart, 
            forMainFrameOnly: true
        )
        userContentController.addUserScript(userScript)
    }

In setupWebViewBridge, you can inject a source script before H5 loads.

For example, inject a function that H5 can call:

window.easyapp = {
    alert: function(message) {
        alert(message);
    }
}

Typically, attach functions to window so JavaScript can call window.easyapp.alert directly.

3. How to inject cookies

The template provides a common class CommonEAWebViewModel with a built-in method:

About CommonEAWebViewModel:

This class abstracts common native-H5 interaction logic such as alerts, internal WebView delegates, cookie injection, etc.

Simply inherit from this class.

/// Set Cookie
/// - Parameters:
///   - request: The request to set the cookie for
///   - name: Cookie name
///   - value: Cookie value
func setCookie(request: URLRequest, name: String, value: String) {
    if let domain = request.url?.host(),
        let cookie = HTTPCookie(properties: [
            HTTPCookiePropertyKey.name: name,
            HTTPCookiePropertyKey.value: value,
            HTTPCookiePropertyKey.domain: domain,
            HTTPCookiePropertyKey.path: "/",
        ])
    {
        configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
    }
}

You can inherit from CommonEAWebViewModel and call setCookie in init:

class OnlineWebViewModel: CommonEAWebViewModel {

    /// Request to the webview
    let request = URLRequest(url: URL(string: "https://easyapp-webview-debugger.vercel.app/")!)

    /// Init
    override init() {
        super.init()
        setCookie(request: request, name: "SampleKey", value: "SampleValue")
    }
}

Why inject cookies?

For example, if the user has logged in within the app, you may want H5 to recognize that login state. Inject a cookie in the app; once H5 reads it, H5 can use the login state.

4. How does native call H5 functions / send messages?

First, H5 must provide the function. In our example project EasyApp-WebView-Debugger,

you can see we register showSwiftUIMessage on window.sample in registerReceiveMessageFunctions:

/**
 * The functions registered here are specifically for SwiftUI client to call
 */
export function registerReceiveMessageFunctions() {
  //@ts-ignore
 window.sample.showSwiftUIMessage = showSwiftUIMessage;
 
  // If you want to receive messages from SwiftUI, you can register your functions here
}

Recommended approach:

Since JavaScript lacks namespaces, we recommend organizing by business modules. If you attach everything to window, ensure names do not collide.

In the example, we register showSwiftUIMessage on sample. Here, sample acts as your business module, preventing name collisions.

If you later add a user module, you can also register a showSwiftUIMessage method there. Module scoping avoids conflicts.

Once JS functions are ready, the client can call them like this:

Task {
    do {
        try await proxy.evaluateJavaScript(
            "window.sample.showSwiftUIMessage('Hello from SwiftUI')")
    } catch {
        print("error: \(error)")
    }
}

See EasyAppSwiftUI/App/Developer/SubPages/OnlineWeb/View/OnlineView.swift for usage.

5. How does JS call native functions / send messages?

Similarly, we use the global window object. We attach a sample object on window and register a doSomething function. You can name functions as you like. We still recommend using a business module as a namespace for maintainability.

/**
 * common post message to SwiftUI
 * @param message message to send to SwiftUI
 */
function commonPostMessage(message: string) {
  // @ts-ignore
  webkit.messageHandlers.sample.postMessage({
    message: message,
  });
}

/**
 * The functions registered here are specifically for sending messages to the SwiftUI client
 */
export function registerSendMessageFunctions() {
  //@ts-ignore
  window.sample.doSomething = function (message: string) {
    // @ts-ignore
    commonPostMessage(message);
  };

  // if your SwiftUI has a function that you want to call, you can register it here
}

After registration, you can call native via window.sample.doSomething:

export function sendSomethisToSwiftUI() {
  // @ts-ignore
  if (window.sample && typeof window.sample.doSomething === "function") {
    //  @ts-ignore
    window.sample.doSomething("Hello from H5");
  }
}

You can find the complete usage in our example project: EasyApp-WebView-Debugger.

On the client side, we use another class, ScriptMessageHandler, dedicated to JS interaction. Script injection is also handled there.

When the client receives a message from H5, this method is triggered:

/// Handle messages from the webview
/// - Parameters:
///   - userContentController: The user content controller
///   - message: The message from the webview   
func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
) {
    // Here, name is the H5 namespace
    guard message.name == "sample" else {
        print("⚠️ Received message from unknown handler: \(message.name)")
        return
    }

    let json = JSON(message.body)

    let formattedJSON = json.rawString() ?? "{}"
    
    messageHandler?(formattedJSON)
}

Note:

message.name is the H5 namespace. It must match the namespace used on the H5 side. This mismatch is a common source of issues—align it explicitly.

You can extract the message sent from H5 here, and then proceed with your own business logic.

Summary

For client-side best practices:

  • You may have multiple modules needing WebView capabilities. We recommend creating a dedicated ViewModel per module, each inheriting from CommonEAWebViewModel to gain shared WebView functionality.
  • Load the WebView URL in the corresponding ViewModel:
   /// Request to the webview
let request = URLRequest(url: URL(string: "https://easyapp-webview-debugger.vercel.app/")!)
  • For native/H5 interactions, create a module-specific XXX_ScriptMessageHandler class. Do not put all logic into the provided ScriptMessageHandler; that class is just an example.
  • Each module’s ScriptMessageHandler should be associated with its ViewModel. Set up the association in the ViewModel:
/// Message handler
let messageHandler = ScriptMessageHandler.shared

configuration.defaultWebpagePreferences.preferredContentMode = .mobile

configuration.userContentController = messageHandler.userContentController
  
  • Let ScriptMessageHandler notify the ViewModel via a closure when receiving messages:
/// Start listening to messages from ScriptMessageHandler
private func startListeningToMessages() {
    messageHandler.messageHandler = { [weak self] message in
        guard let self = self else { return }
        // do your business logic here
    }
}
  • On the H5 side, remember: since JS has no namespaces, organize by business modules instead of hanging everything directly under window to avoid name collisions as modules grow.

Last updated on