Making Javascript feel like native iOS with WKWebView
Embedded web applications can offer an extra degree of functionality and flexibility when included in combination with the native code. They are commonly used to display information such as documents but beyond that embedded web components can be truly interactive and enhance the user experience. As an added bonus, they can be updated without resubmitting the app to the App Store.
In the case where a web component of an app is interactive it should truly feel like a native experience. Meaning, instead of behaving like a web page it should behave as if it was part of the native app itself.
So thats the goal: build a native web view component that makes web content feel like it’s part of the app.
TL;DR — Gist of the code is available at the end of this post.
Design Considerations
- Viewport — Modern web applications are often (and should be) responsive. This is an important consideration when designing for a smaller screen size, and even more so considering iOS apps built with Auto-layout may grow or shrink the size of the web view as part of the app UI.
- Scrolling — Web apps behaving like native components should not scroll like a web page. If they are meant to scroll (such as a table view), they should adopt acceleration. In almost all cases, they should not scroll in two directions.
- Magnification — Web pages can be magnified. But a user should not be able to magnify a native component of an app (by pinching for example).
- Selection and Callouts — Non-text native components are generally not highlightable and callouts(the things that show options like “copy” or “select all”) should also be disabled.
So We Have to Get Rid of All That Stuff…
Enter WKWebView
WKWebView is an iOS component that allows us to present web content inside an iOS app. You can read more about it here http://nshipster.com/wkwebkit/. This is the class used to create the native web component.
By default WKWebView adopts many of the expected behaviors of a normal browser. As in all that unwanted stuff 🙄… so we have to take care of it.
JS Injection
A really cool part about WKWebView is it allows javascript to be injected directly as scripts. So the webpage setup can take place from the native code when the web view is set up.
Note: This can all be done in the JS/HTML itself but injecting it through the native guarantees the behavior we want will exist for any content populated in the web view.
Viewport
This script adds an element to the head of a document. It specifies how the viewport should lay itself out. Content should stretch to device width, scale should always be 1.0, and content is not user scalable.
let viewportScriptString = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); meta.setAttribute('initial-scale', '1.0'); meta.setAttribute('maximum-scale', '1.0'); meta.setAttribute('minimum-scale', '1.0'); meta.setAttribute('user-scalable', 'no'); document.getElementsByTagName('head')[0].appendChild(meta);"Disable Selection & Callout
let disableSelectionScriptString = "document.documentElement.style.webkitUserSelect='none';"
let disableCalloutScriptString = "document.documentElement.style.webkitTouchCallout='none';"
Create Scripts & Initialize WKWebView with Configuration
Now the script strings are used to initialize a WKWebView with the custom configuration.
// 1 - Make user scripts for injection
let viewportScript = WKUserScript(source: viewportScriptString, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
let disableSelectionScript = WKUserScript(source: disableSelectionScriptString, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
let disableCalloutScript = WKUserScript(source: disableCalloutScriptString, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
// 2 - Initialize a user content controller
// From docs: "provides a way for JavaScript to post messages and inject user scripts to a web view."
let controller = WKUserContentController()
// 3 - Add scripts
controller.addUserScript(viewportScript)
controller.addUserScript(disableSelectionScript)
controller.addUserScript(disableCalloutScript)
// 4 - Initialize a configuration and set controller
let config = WKWebViewConfiguration()
config.userContentController = controller
// 5 - Initialize webview with configuration
let nativeWebView = WKWebView(frame: CGRect.zero, configuration: config)
WKWebView Options
Some more web view set up that takes care of scrolling, interaction, etc.
// 6 - Webview options
// Make sure our view is interactable
nativeWebView.scrollView.scrollEnabled = true
// Things like this should be handled in web code
nativeWebView.scrollView.bounces = false
// Disable swiping to navigate
nativeWebView.allowsBackForwardNavigationGestures = false
// Scale the page to fill the web view
nativeWebView.contentMode = .ScaleToFill
Scroll View Delegate
Last but not least, magnification needs to be disabled. This part will be handled in the WKWebView’s scrollView. To turn off zooming on the scroll view, we return nil for viewForZoomingInScrollView(:_) in the scroll view delegate. For convenience I’ve made a singleton class to use as a delegate.
// 7 - Set the scroll view delegate
nativeWebView.scrollView.delegate = NativeWebViewScrollViewDelegate.shared
}
// 8 - Scroll view delegate class
class NativeWebViewScrollViewDelegate: NSObject, UIScrollViewDelegate { // MARK: - Shared delegate
static var shared = NativeWebViewScrollViewDelegate()
// MARK: - UIScrollViewDelegate
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return nil
}
}
Code
The web view is now set up to feel and behave just like a native component 👍. Only thing left to do is make the web content.
Here’s the full code as a gist.