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:

2. How does native inject scripts?
See ScriptMessageHandler.swift for an example:
/// 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
ViewModelper module, each inheriting fromCommonEAWebViewModelto 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_ScriptMessageHandlerclass. Do not put all logic into the providedScriptMessageHandler; that class is just an example. - Each module’s
ScriptMessageHandlershould be associated with itsViewModel. Set up the association in theViewModel:
/// Message handler
let messageHandler = ScriptMessageHandler.shared
configuration.defaultWebpagePreferences.preferredContentMode = .mobile
configuration.userContentController = messageHandler.userContentController
- Let
ScriptMessageHandlernotify theViewModelvia 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
windowto avoid name collisions as modules grow.
Last updated on