How To Integrate a Web Component Into a Mobile App While Preserving Native UX
How to integrate a Web Component into a Mobile (iOS) application and don't break a "Native" UX spirit. What should you look at to keep the app consistent.
Join the DZone community and get the full member experience.
Join For FreeThis article is designed for developers who strive to maintain a native mobile experience as much as possible. While we all (hopefully) prefer to preserve the native spirit, there are instances when we can't avoid integrating web components into our mobile application. This might be driven by business needs, time constraints, or simply common sense.
Here are a few examples:
- Integrating a user onboarding journey already available for web customers into the product
- Integrating a widget with complex chart or graph representations from the web
- Integrating web-based ads and promotional materials
We may not want a full-screen web view but rather a mix of native elements on the screen. Our target picture is as follows:
The web view can be integrated as a remote URL or embedded component into the application. This means we will have HTML, CSS, JavaScript, and all necessary assets directly within the resources of our application or package.
In my case, I aimed to integrate an EPUB book reader into a native iOS application. Here's a brief explanation of my motivation and why I ultimately chose web integration:
I wanted to preserve as much of the native user experience as possible. To integrate an EPUB book, several challenges need to be addressed:
- Parsing the EPUB document
- Implementing rendering
- Controlling pagination and reading state
- Selecting and highlighting words
All the challenges listed above can be solved without web reader integration, but it takes time. When I began comparing the pros and cons, I found that the web option was quite appealing.
Pros
- One implementation for all platforms. If I decide to implement an Android application later, I already have a component that I can integrate there as well, without having to start from scratch or find existing solutions and customize them to achieve the desired look and feel.
- There are numerous web libraries with EPUB reader implementations.
- It's simpler to implement the functionality I need in a web-based component.
Cons
- The web is not native and the UX might not be as good as it could be with a fully native implementation.
As we can see, the main issue we face with a web solution is the risk of losing the native experience. The purpose of this article is to highlight some crucial points and solutions for addressing or at least minimizing this issue.
Keep Focus on the Goal
The key to successful web integration and making it as unnoticeable as possible is to stay focused on the initial goal. Ask yourself: Why am I doing this? And try to identify the real reason behind it.
In my case, it was EPUB parsing and book rendering. This means that my web view should only present the text and images from the book to the user. I shouldn't have any additional UI elements on the web side, just pure text. Text looks the same in native and web components, so that I can use the same fonts for both the native and web parts. This means that I can not only minimize potential negative impacts on UX but eliminate them.
However, I can only achieve this if I maintain focus on my original goal. It might be simpler to have some UI elements to configure font and style and present some messages to the user inside the web view, but this would only obscure my original goal, and I would lose sight of it in the end.
Inject Initial Configuration on Bootstrap
Although we've established that only text and images should be presented inside the web part, we still need to take care of many aspects and details that can disrupt the UX. One obvious issue we might encounter is that the web view is unaware of our current application and device configuration. For instance, it will always present content in a light theme and will not respect the device's appearance. It also won't know anything about the application's locale or the language that should be used for some UI elements (if such elements exist).
For more advanced and complex integration, you might also need to configure the analytics tool destination, transfer the user's session, etc.
The simplest solution is to inject all necessary values as URL parameters at the bootstrap of the web view, parse them internally, and prepare the content to be fully aligned with the native part of the application.
Don’t Show What’s Not Ready
There might be a gap between creating the web view and when the user sees the content. Since we're dealing with something we don't fully control, it's better not to show it before we're certain the content is ready. We might encounter glitches during the loading of web content, CSS styles might be applied with a slight delay, or some custom loading states inside the web component might have a very different UI/UX compared to what we have in the application.
The solution might be the following:
- Control the loading state of the web view in the native code.
- It's even better to have a custom event from the web side that notifies the mobile app when everything is ready.
- Do not show the web component until you receive this event, or show some placeholder/loading on the mobile side using native components.
Avoid Full-Screen Covers, Dialogs, and Loading Indicators Inside the Web
Let's say you need to show a loading state with a loading indicator and a dimmed background for the content. If you do this inside the web view and don't use it as a full-screen cover, the result can be very poor. The same applies to any other UI elements that should be presented over some content: alerts, dialogs, bottom sheets, etc. A better approach would be to use events for all kinds of presentation states you want to handle and keep the implementation on the mobile native application side.
Try To Avoid Additional Dialogs and Transitions
For my EPUB reader, I wanted to implement an option to change the font style and size initially and later add the ability to choose highlight colors. One option could be to use an internal web view dialog with such parameters, but then I would need to have some transition to a separate screen inside the web view, which wouldn't be ideal, or use a pop-up dialog, which is not a flexible option either. I chose to have a native UI for it and call a JS function with the new font style and size as parameters when the user applies a new configuration.
How To Keep Communication Clean
I won't dive into the details of how to organize communication between the web and mobile, as there are many resources available on this topic. You will likely use the following:
- URL params for initial setup.
- Inject JavaScript code to execute functions inside the web.
- Handle webkit messages posted from the web side.
The first point is straightforward.
For the second one, I recommend implementing a sort of public interface on the web side. For example, here's how the index.html
looks in my EPUB reader example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="index.css" />
<link rel="stylesheet" href="themes.css" />
</head>
<body>
<div id="book"></div>
<script src="bundle.js"></script>
<script>
const urlParams = new URLSearchParams(window.location.search);
const theme = urlParams.get("theme");
if (theme) {
if (theme === "dark") {
document.body.classList.add("dark");
} else {
document.body.classList.add("light");
}
}
function highlightWords(value) {
try {
const wordsObject = JSON.parse(value);
const event = new CustomEvent("highlightWords", {
detail: wordsObject,
});
window.dispatchEvent(event);
} catch {}
}
function changeFontSize(value) {
const event = new CustomEvent("changeFontSize", { detail: value });
window.dispatchEvent(event);
}
function changeTheme(value) {
const event = new CustomEvent("changeTheme", { detail: value });
window.dispatchEvent(event);
}
function goToPage(value) {
const event = new CustomEvent("goToPage", { detail: value });
window.dispatchEvent(event);
}
function loadBook(value) {
try {
const bookData = JSON.parse(value);
const event = new CustomEvent("loadBook", { detail: bookData });
window.dispatchEvent(event);
} catch {}
}
function changeFont(value) {
const event = new CustomEvent("changeFont", { detail: value });
window.dispatchEvent(event);
}
function unhighlightWords(value) {
try {
const wordsObject = JSON.parse(value);
const event = new CustomEvent("unhighlightWords", {
detail: wordsObject,
});
window.dispatchEvent(event);
} catch {}
}
</script>
</body>
</html>
You could also directly inject window.dispatchEvent(event);
on the mobile side and does not have any additional functions as presented above, but I believe it's very useful to keep it as an interface for your web component. A developer can open the file and quickly understand what this web component does.
For the third point of handling events/messages from the web view, I've created a wrapper that handles all incoming messages:
func didReceiveMessage(message: Any) {
guard
let dict = message as? [String: AnyObject],
let event = dict["event"] as? String,
let eventType = BookEventType(rawValue: event),
let data = try? JSONSerialization.data(withJSONObject: dict, options: [])
else {
return
}
switch eventType {
case .saveWord:
guard let word: String = parseEvent(data: data) else {
return
}
eventsHandler?.onSelect(word: word.alphanumeric)
case .onBookLoaded:
eventsHandler?.onBookLoaded()
case .setSavedData:
guard let savedData: BookSavedData = parseEvent(data: data) else {
return
}
eventsHandler?.onUpdated(savedData: savedData)
}
}
private func parseEvent<T: Decodable>(data: Data) -> T? {
return try? JSONDecoder().decode(BookEvent<T>.self, from: data).value
}
Where BookEvent
is a generic placeholder for any incoming message from the web:
public struct BookEvent<T: Decodable>: Decodable {
public let value: T
}
And BookEventType
is an enum that contains all possible events we might receive from the web:
public enum BookEventType: String, Codable {
case saveWord
case onBookLoaded
case setSavedData
}
I also recommend hiding all these details from the application itself. It's better to create a separate Swift Package
for such integration and use it at the application level. The application shouldn't even know if it's working with a web or native reader. Maybe in the future, I will decide to rewrite it as fully native; for example, such a decision shouldn't change anything between this package and the app. In my case, I provide such methods in the package:
@MainActor
public func change(theme: Theme) async throws {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
webManager.callJS(function: "changeTheme('\(theme.rawValue)')") { result in
switch result {
case .success:
continuation.resume(returning: Void())
case .failure(let error):
continuation.resume(throwing: ReaderError.changeThemeFailed)
}
}
}
}
That allows me to keep integration very simple:
let readerController = ReaderViewController(theme: .dark, eventsHandler: self)
...
try? await readerController.loadBook(url: url)
If you need more details, check the package implementation.
Conclusion
Maintaining a consistent UX in your mobile app is crucial. Web integration can disrupt this consistency, and we need to minimize such effects. Following several simple rules can significantly contribute to this goal:
- Only show the web component when it's ready.
- Try to move all additional UI elements from the web to mobile and replace them with events.
- Avoid transitions and additional dialogs inside the web.
- Implement such integration as separate packages and maintain a clean interface.
Opinions expressed by DZone contributors are their own.
Comments