WKWebView
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
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
and the rest of the Web
framework.
WKWeb
is the centerpiece of the modern WebKit API
introduced in iOS 8 & macOS Yosemite.
It replaces UIWeb
in UIKit and Web
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
was one of the most significant announcements at WWDC 2014.
What was once a single class and protocol with UIWeb
& UIWeb
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
has been the preferred API since iOS 8.
But if your app still hasn’t made the switch,
be advised that
UIWeb
and Web
are formally deprecated
in iOS 12 and macOS Mojave,
and you should update to WKWeb
as soon as possible.
To help make that transition,
here’s a comparison of the APIs of UIWeb
and WKWeb
:
UIWebView | WKWebView |
---|---|
var scroll |
var scroll |
var configuration: WKWeb |
|
var delegate: UIWeb |
var UIDelegate: WKUIDelegate? |
var navigation |
|
var back |
Loading
UIWebView | WKWebView |
---|---|
func load |
func load(_ request: URLRequest) -> WKNavigation? |
func load |
func load |
func load |
|
var estimated |
|
var has |
|
func reload() |
func reload() -> WKNavigation? |
func reload |
|
func stop |
func stop |
var request: URLRequest? { get } |
|
var URL: URL? { get } |
|
var title: String? { get } |
History
UIWebView | WKWebView |
---|---|
func go |
|
func go |
func go |
func go |
func go |
var can |
var can |
var can |
var can |
var loading: Bool { get } |
var loading: Bool { get } |
Javascript Evaluation
UIWebView | WKWebView |
---|---|
func string |
|
func evaluate |
Miscellaneous
UIWebView | WKWebView |
---|---|
var keyboard |
|
var scales |
|
var allows |
Pagination
WKWeb
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 }
WKWeb View Configuration
Refactored into The following properties on UIWeb
have been factored into a separate configuration object,
which is passed into the initializer for WKWeb
:
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
is how interaction and data can be passed back and forth
between an app and its web content.
Injecting Behavior with User Scripts
WKUser
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
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
,
which is set on the WKWeb
object
passed into the initializer for WKWeb
.
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
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:
// 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
,
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
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
delegate method
to call the take
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
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.