Making Javascript feel like native iOS with WKWebView

Max Campolo
Jul 28, 2016 · 4 min read

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 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.


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 = "'none';"let disableCalloutScriptString = "'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 injectionlet 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 scriptscontroller.addUserScript(viewportScript)controller.addUserScript(disableSelectionScript)controller.addUserScript(disableCalloutScript)// 4 - Initialize a configuration and set controllerlet config = WKWebViewConfiguration()config.userContentController = controller// 5 - Initialize webview with configurationlet nativeWebView = WKWebView(frame:, 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 delegatenativeWebView.scrollView.delegate = NativeWebViewScrollViewDelegate.shared}// 8 - Scroll view delegate classclass NativeWebViewScrollViewDelegate: NSObject, UIScrollViewDelegate {         // MARK: - Shared delegate         static var shared = NativeWebViewScrollViewDelegate()         // MARK: - UIScrollViewDelegate     
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return nil


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.

Max Campolo

Written by

Product Manager, iOS Engineer, Gamer, Musician.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade