iOS has a complicated relationship with the web. And it goes back to the very inception of the platform over a decade ago.

Although the design of the first iPhone seems like a foregone conclusion today, the iconic touchscreen device we know and love today was just one option on the table at the time. Early prototypes explored the use of a physical keyboard and a touchscreen + stylus combo, with screen dimensions going up to 5×7”. Even the iPod click wheel was a serious contender for a time.

But perhaps the most significant early decision to be made involved software, not hardware.

How should the iPhone run software? Apps, like on macOS? Or as web pages, using Safari? That choice to fork macOS and build iPhoneOS had widespread implications and remains a contentious decision to this day.

Consider this infamous line from Steve Jobs’ WWDC 2007 keynote:

The full Safari engine is inside of iPhone. And so, you can write amazing Web 2.0 and Ajax apps that look exactly and behave exactly like apps on the iPhone. And these apps can integrate perfectly with iPhone services. They can make a call, they can send an email, they can look up a location on Google Maps.

The web had long been a second-class citizen on iOS, which is ironic since the iPhone is largely responsible for the mobile web as it exists today. UIWeb View was massive and clunky and leaked memory like a sieve. It lagged behind Mobile Safari, unable to take advantage of its faster JavaScript and rendering engines.

However, all of this changed with the introduction of WKWeb View and the rest of the Web Kit framework.

WKWeb View is the centerpiece of the modern WebKit API introduced in iOS 8 & macOS Yosemite. It replaces UIWeb View in UIKit and Web View in AppKit, offering a consistent API across the two platforms.

Boasting responsive 60fps scrolling, built-in gestures, streamlined communication between app and webpage, and the same JavaScript engine as Safari, WKWeb View was one of the most significant announcements at WWDC 2014.

What was once a single class and protocol with UIWeb View & UIWeb View Delegate has been factored out into 14 classes and 3 protocols in the WebKit framework. Don’t be alarmed by the huge jump in complexity, though — this new architecture is much cleaner, and allows for a ton of new features.

Migrating from UIWebView / WebView to WKWebView

WKWeb View has been the preferred API since iOS 8. But if your app still hasn’t made the switch, be advised that UIWeb View and Web View are formally deprecated in iOS 12 and macOS Mojave, and you should update to WKWeb View as soon as possible.

To help make that transition, here’s a comparison of the APIs of UIWeb View and WKWeb View :

UIWebView WKWebView var scroll View: UIScroll View { get } var scroll View: UIScroll View { get } var configuration: WKWeb View Configuration { get } var delegate: UIWeb View Delegate? var UIDelegate: WKUIDelegate? var navigation Delegate: WKNavigation Delegate? var back Forward List: WKBack Forward List { get }

Loading

UIWebView WKWebView func load Request(request: URLRequest) func load(_ request: URLRequest) -> WKNavigation? func load HTMLString(string: String, base URL: URL?) func load HTMLString(_: String, base URL: URL?) -> WKNavigation? func load Data(_ data: Data, mime Type: String, character Encoding Name: String, base URL: URL) -> WKNavigation? var estimated Progress: Double { get } var has Only Secure Content: Bool { get } func reload() func reload() -> WKNavigation? func reload From Origin(Any?) -> WKNavigation? func stop Loading() func stop Loading() var request: URLRequest? { get } var URL: URL? { get } var title: String? { get }

History

UIWebView WKWebView func go To Back Forward List Item(item: WKBack Forward List Item) -> WKNavigation? func go Back() func go Back() -> WKNavigation? func go Forward() func go Forward() -> WKNavigation? var can Go Back: Bool { get } var can Go Back: Bool { get } var can Go Forward: Bool { get } var can Go Forward: Bool { get } var loading: Bool { get } var loading: Bool { get }

Javascript Evaluation

UIWebView WKWebView func string By Evaluating Java Script From String(script: String) -> String func evaluate Java Script(_ java Script String: String, completion Handler: ((Any Object?, NSError?) -> Void)?)

Miscellaneous

UIWebView WKWebView var keyboard Display Requires User Action: Bool var scales Page To Fit: Bool var allows Back Forward Navigation Gestures: Bool

Pagination

WKWeb View currently lacks equivalent APIs for paginating content.

var pagination Mode: UIWeb Pagination Mode

var pagination Breaking Mode: UIWeb Pagination Breaking Mode

var page Length: CGFloat

var gap Between Pages: CGFloat

var page Count: Int { get }

Refactored into WKWeb View Configuration

The following properties on UIWeb View have been factored into a separate configuration object, which is passed into the initializer for WKWeb View :

var allows Inline Media Playback: Bool

var allows Air Play For Media Playback: Bool

var media Types Requiring User Action For Playback: WKAudiovisual Media Types

var suppresses Incremental Rendering: Bool

JavaScript ↔︎ Swift Communication

One of the major improvements over UIWeb View is how interaction and data can be passed back and forth between an app and its web content.

Injecting Behavior with User Scripts

WKUser Script allows JavaScript behavior to be injected at the start or end of document load. This powerful feature allows for web content to be manipulated in a safe and consistent way across page requests.

As a simple example, here’s how a user script can be injected to change the background color of a web page:

let source = """ document.body.style.background = " # 777 "; """ let user Script = WKUser Script ( source : source , injection Time : . at Document End , for Main Frame Only : true ) let user Content Controller = WKUser Content Controller () user Content Controller . add User Script ( user Script ) let configuration = WKWeb View Configuration () configuration . user Content Controller = user Content Controller self . web View = WKWeb View ( frame : self . view . bounds , configuration : configuration )

When you create a WKUser Script object, you provide JavaScript code to execute, specify whether it should be injected at the start or end of loading the document, and whether the behavior should be used for all frames or just the main frame. The user script is then added to a WKUser Content Controller , which is set on the WKWeb View Configuration object passed into the initializer for WKWeb View .

This example could easily be extended to perform more significant modifications, such as changing all occurrences of the phrase “the cloud” to “my butt”.

Message Handlers

Communication from web to app has improved significantly as well, with the introduction of message handlers.

Like how console.log prints out information to the Safari Web Inspector, information from a web page can be passed back to the app by invoking:

window . webkit . message Handlers . < # name # > . post Message ()

What’s really great about this API is that JavaScript objects are automatically serialized into native Objective-C or Swift objects.

The name of the handler is configured in add(_:name) , which registers a handler conforming to the WKScript Message Handler protocol:

class Notification Script Message Handler : NSObject , WKScript Message Handler { func user Content Controller ( _ user Content Controller : WKUser Content Controller , did Receive message : WKScript Message ) { print ( message . body ) } } let user Content Controller = WKUser Content Controller () let handler = Notification Script Message Handler () user Content Controller . add ( handler , name : "notification" )

Now, when a notification comes into the app (such as to notify the creation of a new object on the page) that information can be passed with:

window . webkit . message Handlers . notification . post Message ({ body : " ... " });

Add User Scripts to create hooks for webpage events that use Message Handlers to communicate status back to the app.

The same approach can be used to scrape information from the page for display or analysis within the app.

For example, if you wanted to build a browser specifically for NSHipster.com, it could have a button that listed related articles in a popover:

JavaScript Swift // document.location.href == "https://nshipster.com/wkwebview" const show Related Articles = () => { let related = []; const elements = document . query Selector All ( " #related a " ); for ( const a of elements ) { related . push ({ href : a . href , title : a . title }); } window . webkit . message Handlers . related . post Message ({ articles : related }); }; let js = "show Related Articles();" self . web View ? . evaluate Java Script ( js ) { ( _ , error ) in print ( error ) } // Get results in a previously-registered message handler

Content Blocking Rules

Though depending on your use case, you may be able to skip the hassle of round-trip communication with JavaScript.

As of iOS 11 and macOS High Sierra, you can specify declarative content blocking rules for a WKWeb View , just like a Safari Content Blocker app extension.

For example, if you wanted to Make Medium Readable Again in your web view, you could define the following rules in JSON:

let json = """ [ { " trigger ": { " if - domain ": " *. medium . com " }, " action ": { " type ": " css - display - none ", " selector ": " . overlay " } } ] """

Pass these rules to compile Content Rule List(for Identifier:encoded Content Rule List:completion Handler:) and configure a web view with the resulting content rule list in the completion handler:

WKContent Rule List Store . default () . compile Content Rule List ( for Identifier : "Content Blocking Rules" , encoded Content Rule List : json ) { ( content Rule List , error ) in guard let content Rule List = content Rule List , error == nil else { return } let configuration = WKWeb View Configuration () configuration . user Content Controller . add ( content Rule List ) self . web View = WKWeb View ( frame : self . view . bounds , configuration : configuration ) }

By declaring rules declaratively, WebKit can compile these operations into bytecode that can run much more efficiently than if you injected JavaScript to do the same thing.

In addition to hiding page elements, you can use content blocking rules to prevent page resources from loading (like images or scripts), strip cookies from requests to the server, and force a page to load securely over HTTPS.

Snapshots

Starting in iOS 11 and macOS High Sierra, the WebKit framework provides built-in APIs for taking screenshots of web pages.

To take a picture of your web view’s visible viewport after everything is finished loading, implement the web View(_:did Finish:) delegate method to call the take Snapshot(with:completion Handler:) method like so:

func web View ( _ web View : WKWeb View , did Finish navigation : WKNavigation ! ) { var snapshot Configuration = WKSnapshot Configuration () snapshot Configuration . snapshot Width = 1440 web View . take Snapshot ( with : snapshot Configuration ) { ( image , error ) in guard let image = image , error == nil else { return } … } }

Previously, taking screenshots of a web page meant messing around with view layers and graphics contexts. So a clean, single method option is a welcome addition to the API.

WKWeb View truly makes the web feel like a first-class citizen. Even if you consider yourself native purist, you may be surprised at the power and flexibility afforded by WebKit.

In fact, many of the apps you use every day rely on WebKit to render especially tricky content. The fact that you probably haven’t noticed should be an indicator that web views are consistent with app development best practices.