스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Discover WKWebView enhancements
WKWebView is the best way to present rich, interactive web content right within your app. Explore new APIs that help you convert apps using WebViews or UIWebViews while adding entirely new capabilities. Learn about better ways to handle JavaScript, fine tune the rendering process, export web content, and more.
리소스
관련 비디오
WWDC21
WWDC20
-
다운로드
Hello and welcome to WWDC.
Hello, everyone. Welcome to "Discover WKWebView Enhancements." My name is Brady Eidson, and I'm an engineer on the WebKit Architecture team. I'm genuinely excited to talk to you today about new features of the WKWebView API. At Apple, we love the Web. We pay great attention to detail, making sure that the WebKit framework is fast, responsive, secure and efficient so that our favorite browser, Safari, can be all of those things.
But we don't only love exploring the Web in a browser. We love seeing web technology used everywhere, and our platforms enable amazing integration of web content with native experiences.
If you primarily need an in-app web browser and don't need deep customization of that experience, SFSafariViewController is the best choice for you and your users. It handles all of the things you'd need to worry about while implementing a basic browser and beyond. Your users get Reader, content blockers, autofill and more, and you get a browser in a box. Add it to your app and give it a URL to load. SafariViewController is easy to use, but powerful because it is built on top of WKWebView.
When you need a higher degree of configurability or are using web content in ways unrelated to browsing, you can also build on WKWebView directly.
WKWebView automatically protects your application's code and data from the complexities of the web platform by isolating web content in a separate process. This enables fast execution of web content while also providing protection against malicious actors.
SafariViewController uses WKWebView because of those benefits, and they are also why, for years, we've strongly recommended WKWebView over other alternatives. Speaking of those other alternatives, WebView and UIWebView were our original APIs for embedding web content. They've served us well, but for all the benefits WKWebView has, they tend to have the parallel drawbacks which is why they've been deprecated for a few years now and why we are striving to drive their usage down to zero. The deprecated WebViews work so differently from WKWebView, it wasn't desirable or even possible to make WKWebView APIs work exactly the same. This difference sometimes requires you to rethink how you do things. Other times, it requires us to add new capabilities to WKWebView. For those developers who have run into challenges transitioning to WKWebView, we are listening to you, and the WKWebView API is still growing. It has each year since its introduction, and it will each year going forward. On that note, I'm going to talk about new features WKWebView has in iOS 14 and macOS Big Sur.
Some will make previously difficult tasks much easier. Some will bring back things missing from the deprecated WebViews. And some are brand-new capabilities.
We have a lot on our plate to cover. First, I'm going to discuss new APIs to help you isolate your app and web content from one another...
then we'll talk about improvements in how you interact with web content via JavaScript...
we'll go over a few APIs to help you fine-tune the rendering of web content in your app, we'll touch on ways to get more value from web content on the native side of things, and finally, we'll touch on a topic that is more and more important each day, respecting privacy.
To help show off these new capabilities, I'm going to tell you a story that is probably familiar to many of you, and that is the story of an app. First, a little background. Like most mature software engineering organizations, the Safari and WebKit teams have many mission-critical internal tools to keep the project going.
One of these critical tools is our internal WebKittens website, to notify team members of the important updates to the department's many cats.
The website has been with us for longer than the iPhone. But of course, once iOS was a thing, somebody took it upon themselves to write the WebKittens app using UIWebView.
Also like most mature software engineering organizations, our department has a lot of duplicated effort in its mission-critical internal tools.
The Pups on Safari website is one of those to stay updated on the department's many dogs.
And like with WebKittens, Pups on Safari also has an aging iPhone app.
For such critical content, having to check multiple places throughout the day was kind of a drag, so I decided to solve that problem by writing Browser Pets, a single feed seamlessly combining both sites natively managing the interactions between them.
Since each of the original apps evolved independently, I started from scratch using WKWebView.
One of the first challenges I ran into was isolating my application's logic from the web content.
The "hello world" moment for my app was showing the WebKittens' news feed by itself. And the first thing I noticed while trying to scroll through the feed was I couldn't. As I scrolled through the page with my finger, the page was fighting me. My experience with the WebKittens website is that it has a scrolling experience heavily customized by JavaScript. That's fine for the page in the browser, but I'm doing deep customization of the experience in my app. I just want the kitties and nothing else. The WKWebView API has always had a way to disable JavaScript by setting the javaScriptEnabled property of WKPreferences to "false." Unless you're writing a general purpose web browser, it's always good to ask yourself if you can disable JavaScript from remote sources, especially those outside of your control. With WKWebView, this can become problematic since most interaction with the content inside the view is by evaluating JavaScript.
So, we've deprecated that setting this year and we've added something new.
By using the allowsContentJavaScript setting on WKWebPagePreferences, you disable only the JavaScript that comes from the web page content itself. In-line scripts, remotely referenced JavaScript files, JavaScript URLs, everything.
But your application's JavaScript will continue working.
If you are not familiar with the WKWebPagePreferences API, it's pretty slick. It's a recent addition, and it allows configuring certain behaviors on a per-navigation basis. I'm already using the newer version of the policy delegate that lets me configure web page preferences so I can present as a mobile device even when running on an iPad.
Here is where I can also disable ContentJavaScript while viewing the main news feed.
With that change, scrolling works as expected.
Now that I can scroll through the news feed at will, I want to make sure viewing the comments on a post works, so let me tap this one here.
And this is the next problem I ran into. I know from using the website and app for years that the bottom half of this page should be full of comments. Instead, it is blank.
I also know from experience the comments are populated by JavaScript. And before you say it, I did not accidentally disable JavaScript on these navigations. It's time to hit up the Web Inspector.
The Web Inspector is a great way to explore what's happening with web content in your WebKit application. For years you've been able to attach to any WebView or JavaScript context in any app you're developing, but we like to remind developers at every opportunity that you can do this, and that it's awesome.
There are a lot of fantastic improvements to the Web Inspector this year, and I encourage you to check out the session about them to learn more.
Back to our bug. I can see here that there is an error in the JavaScript console.
Let's take a closer look at that.
The web page expects the symbol "commentDetails" to be a function, but it isn't.
When looking at the page's JavaScript, I can clearly see they declare this function, but the Web Inspector is telling us that it's null.
I searched for the symbol in my app and found this code.
Coincidentally, my application code also has a JavaScript symbol named commentDetails injected into the web page. And when I evaluate my JavaScript, I overwrite their function with an unrelated variable.
This type of conflict is an ever-present concern when working with WKWebViews. Your application can accidentally conflict with the web content. The web content can accidentally conflict with your app. Malicious web content can conflict with your application on purpose by trying to change its behavior or steal sensitive information from it. These are all possible as long as our JavaScript and the web page's JavaScript run in the same world. To fix this, we'll need an isolated place for our JavaScript to run separate from the application JavaScript. Our own global object.
And that is where WKContentWorld comes in. A WKContentWorld is an isolated sandbox for JavaScript to run in.
If you are familiar with JavaScript, it's like having your own separate window object for the same page content. There is a page world representing the web content itself, and then there are client worlds representing one or more homes for your application's JavaScript.
Your application's JavaScript run in a client world can still do things like call built-in DOM APIs on the page or change the DOM itself, but it will never see the application state set up by the page's JavaScript.
Likewise, the page's JavaScript will never see yours.
To fix this in my application, I'm going to use the default client world for all of my evaluateJavaScript calls.
And now that my app's JavaScript and the JavaScript on the page no longer conflict, there's those comments. Great. I showed you adopting WKContentWorld with evaluateJavaScript, but now you can also inject WKScriptMessageHandlers into a specific content world to isolate them as well.
I think I've done a pretty good job protecting my app and web content from one another. The next step in developing Browser Pets involved a lot more JavaScript. To help keep that under control, I used some new ways of managing the JavaScript that my app uses.
The WebKittens website is rather static, so it was a great starting point for our combined news feed.
I injected some JavaScript to clean up its look a little bit and to prepare it to integrate the Pups on Safari web content.
The Pups on Safari site has a JavaScript API for fetching posts, and that's what I've used to get the combined news feed up and running, and here's some of the hoops I had to jump through.
When I wanted to reuse the same JavaScript but with different values, you know, kind of like calling a function with parameters, I needed to construct an entirely new JavaScript string each time.
Another awkward thing was passing data. These strings to represent zero? Elsewhere in my application code, they are natively integers, but I have to convert them into strings so I can build a JavaScript string out of the string components. Much worse than those is how I manually add JavaScript strings for each entry in this native dictionary. What I'd love is for the ability to reuse the same constant JavaScript string as a function with named parameters, and to not worry about those parameters getting serialized or deserialized incorrectly.
And those are just a few things that the new callAsyncJavaScript API does. Let's see what using it does to that code.
I can write JavaScript naturally without having to construct a string from arguments. I can name whatever arguments I want for my JavaScript string and provide their values with the call to callAsyncJavaScript. Serialization and deserialization of argument types happens automatically. A Cocoa string becomes a JavaScript string. A number remains a number. A Cocoa dictionary becomes a JavaScript object as I've highlighted here.
This little utility method I've written can easily be reused with different argument values like this.
You might notice in my example that the JavaScript has an explicit return value. That's another difference between this and evaluateJavaScript. If I don't explicitly return a value, then my completion handler receives "undefined" as the result. But there is another really important trick hiding up its sleeve. If your JavaScript returns a promise, then your completion handler is not called right away. Instead, it waits for the promise to resolve and is called with the result of that fulfillment. This code returns the promise that is returned from calling the built-in fetch method, which means that my completion handler isn't called until the bytes for that resource have loaded over the network.
Just one tiny example of how this makes working with modern asynchronous JavaScript seamless. CallAsyncJavaScript is a great addition to evaluate JavaScript, but there's more ways my app uses JavaScript besides my native code evaluating it. The WebKittens folks built in some cool features. For example, if a new comment is being added to a post server-side, the page is notified and lets you know. It even fires a comment event for other JS on the page to listen to.
I wanted my native app to listen for new comments too so I can notify my native code, and I do that using WKScriptMessageHandler.
This API was originally designed as a one-way communications channel. To make it bidirectional to handle a reply to your script message requires some extra code.
For example, tracking each message with an identifier that can be used to look up a later reply. The more sophisticated your messaging system, the more involved your message tracking mechanism needed to be, more code you have to write and debug, and there's some race conditions you may never be able to solve.
There's two new changes this year to improve this. First, postMessage used to return "undefined" in JavaScript as there is no meaningful return value to be had.
Now it returns a promise.
Second, there's a new form of the WKScriptMessageHandler protocol that you can use which gets a completion handler attached to each message it receives.
When you call that completion handler, the promise from the postMessage is resolved.
That JavaScript code waiting for a message reply now boils down to this-- handling the reply to the message through the resolution of the promise, and that's a good thing.
At this point, I feel really great about my application's JavaScript and how well it cleaned up, so I want to turn my attention to making sure the cats and dogs are all displaying as they should. I was able to apply a few more new APIs that we have to allow for more flexible rendering.
This is relatively old web content from before when the phrase "responsive design" had even been coined. The original WebKittens app looked pretty good on early iPhones because the authors had tweaked it for that specific purpose.
This is how viewing a comment on the original iPhone looks.
This is how viewing a WebKittens comment looks on a modern iPhone, much larger than when the WebKittens app was originally written. You can see the beginnings of a native UI I'm adding to Browser Pets to allow returning from a post to the news feed.
The web content itself looks passable, but there's also lots of negative space. It looks more like a 2005-era website in a browser.
I think I can adapt the site as is to make better use of the space to make better use of the screen and to fit in better with native UI.
I decided to see if I could throw some CSS at the problem to fill my users' screens. The CSS zoom property has been with WebKit for a dozen years, and I think it might work here applied to the entire body element.
As long as the zoom value is big enough, it caps to take up the entire viewport width which is perfect in effect. But the way I'm doing it here is a bit clunky.
First, even though I'm evaluating the JavaScript in my own client world, that doesn't change the fact that I'm changing the CSS in ways visible to the page. I don't know if it would cause issues for WebKittens, but you can easily imagine how it might. Second, I chose to do this manually on each navigation, so I get into a bit of a race. Will the content display before my JavaScript-to-override-zoom is executed? Unclear.
Fortunately, we have a solution for that in the form of the new pageZoom property on WKWebView. This is actually the same property that drives command-plus and command-minus full-page-zoom in Safari. And I think a lot of developers will find it useful in a lot of scenarios.
By using it on my WKWebView, I can get the exact same effect as changing CSS with JavaScript but without any of the drawbacks of doing so. So much code removed and more predictable behavior.
Next, let's take a look at a Pups on Safari comment. These headers and footers that you see were meant for display on the website as viewed in a browser. They're not really what I want. I have native UI to show, not these artifacts from the site.
To explore my options for removing them, I decided to learn more about them in the Web Inspector.
I noticed the elements have these classes, pup-header and pup-footer. Let's dig a little deeper and see where those are used.
So they match this custom media query for a no-header-and-footer-device.
One usually sees only a handful of media types in CSS, "Screen," "Print" and so on, and this is not one of them.
I did a little digging amongst my colleagues, and I found out that before there was a Pups app for iPhone, there was also a Mac app that used the original Mac WebView.
WebView in the Mac had this great feature where you could tell it to present itself as a particular media type. Commonly used to pretend to be a printer when you were really a screen, it could also be used for custom types. So, depending on the context within your app, the same content could have different presentation.
And we're bringing that feature forward to WKWebView on all platforms this year.
By setting a custom media type on my WKWebView, I can easily get rid of those elements and also adopt any other styles that might come along for a custom app like mine, all without JavaScript, and globally instead of per navigation.
Now that I've spent some time fine-tuning how things look in my app, I wanted to explore other features I could add that focus on the web content itself. Fortunately, there's some relevant new APIs this year for me to explore.
Looking back at that last Pups on Safari post, this was a popular post with a lot of comments.
I remember Beth made this hilarious joke in those comments, but scrolling through to find it seems cumbersome. I'd rather have a "find in page" feature, but how will I implement it? There are some great JavaScript find utilities out there that might make sense if I needed deep customization of the find experience. But WKWebView now has an easy-to-use find functionality that behaves like others on the platform, and that's just what I'm looking for.
I have a few things I can configure about the find like any find in AppKit and UIKit APIs-- the direction, case sensitivity, wrapping. Or without any configuration, it will just use sensible defaults.
If a result is found, it is selected and scrolled into view.
Voilà.
That joke is just not quite as funny as I remembered it. Another thing a proper app needs to do is share content.
And I can think of no content more worth sharing than the cutest web kittens and the most adorable pups on Safari.
For a few years, WKWebView has supported the ability to take a bitmap snapshot of its contents. This is useful in so many situations.
But it is limited to the on-screen content no matter how much content is actually there.
So if we add a feature to zoom in on the full-resolution version of this photo, for example, we could only capture what's visible. And this is not to mention, depending on your goal, bitmapped images might be a poor fit.
I think another great way of sharing this content would be as a PDF, and this year, WKWebView will support just that.
You can configure a few things about how you want to capture the PDF, or if you just want a PDF of all the content in the view, including content not currently visible on the screen, call createPDF with the default configuration. This code... will share this entire post, including all the comments in a searchable text form in one PDF document that would look like this.
I told you it was a popular post.
Another type of content snapshot that I often find useful when working with WebKit is the WebArchive. A web archive is an actual rich snapshot of the current DOM of the web content and the sub-resources it would need to render. In addition to being a file format supported since the first public release of the WebKit framework, it is also the native pasteboard format for web content on all of our platforms.
When I was first debugging issues with the combined news feed in Browser Pets, I wanted to leverage WebArchives to help me.
While WKWebView has been able to load WebArchive files since its first release, it hasn't been able to save them.
Until now. With createWebArchiveData, I can now take that snapshot of web content for later debugging and testing.
Like taking a bitmap snapshot or creating a PDF, it's a simple method of requesting the WebArchive data and getting the result in a completion handler. I can save this data off to disk for later use, put it on the pasteboard, or even load it into another WKWebView right now.
As I mentioned, WKWebView has always been able to load WebArchive files. I've heard from a few developers that they thought that was not the case, so I just wanted to share the code I used to load a WebArchive, which is load(data with the appropriate mimeType.
With bitmap snapshots, PDFs and web archives, we have a growing number of ways to get stuff out of a WKWebView. And there is one more important one. As I mentioned before, WebKittens and Pups on Safari are mission-critical tools. And some team members even rely on the ability to print out their favorite pets on actual paper for proper motivation throughout the day.
Since Browser Pets is brand new, I am of course using SwiftUI, and I've been able to build a Mac version of the app. But I know printing is important to these team members. Printing WKWebViews has been possible on iOS for a while, but Mac developers have not been so lucky, until macOS Big Sur. If this interests you, go looking for it in your SDK, and give it a whirl.
Before we get to a few more APIs that help you on the app side of things, I know many of you are also focused on the web content itself. We're always working on new Web-facing features that can shine on our platforms, and this year is no exception. Once we're done here, I encourage you to take a look at the "What's New for Web Developers" session.
Finally, I'm happy to share with you exciting announcements regarding user privacy and how they affect developers using WKWebView.
You might have heard Apple say this before, but just in case, I'll repeat it now. We firmly believe that privacy is a fundamental human right, and that belief is legitimately one of our core values.
We've designed iOS and the App Store with this in mind. We strive to prevent any native app from using private information without user consent, not only at the policy level, but also at a technical level, and we get better at this every year. The Web is different. When users browse the wild web, they are often being watched by at least one party. There is no curator, and some web technologies seem to have evolved to encourage tracking users instead of preventing it.
We realized that with WebKit, we could do something about this.
We started working on Intelligent Tracking Prevention, or ITP.
ITP uses various client-side heuristics and machine learning to identify, classify and thwart trackers.
We've improved it continuously since we added it to Safari in 2017, and we won't stop. Since we added ITP to Safari, many WKWebView developers have been asking us to use it in their apps.
And we're happy to announce that in iOS 14 and macOS Big Sur, ITP is enabled by default on all WKWebView apps. Users have the control here. For example, if you have a general purpose web browser, your users might need to disable ITP for compatibility with a website you don't control. Most people will not fall into this situation, but there is API for you to point users towards disabling it just like we've seen with Safari.
Intelligent Tracking Prevention has proven to be a powerful way to protect user privacy in the Web, but we didn't stop there. While trying to think of other ways to protect users on the Web, we identified three common ways web content is used in an app.
The easiest to understand, perhaps, is the general purpose web browser like Safari.
The primary purpose of these apps is to show web content from any source and to help their user manage the things important to them related to the task of web browsing.
On the opposite end of the spectrum from a general web browser is an app that stays in the confines of only one or a few sites. No matter their balance of native UI and displaying web content, the web content itself is part of the core implementation of the app.
And then there's the in-app browser. Often starting from a domain specific to the app, they aggregate content from a lot of different sources, usually allowing users to start browsing from any of them. A prime example is a social media news feed, much like Browser Pets.
For these last two types of app, WKWebView has a new feature called App-bound domains. The idea is simple. You specify which domains are the core part of the implementation of your app.
This empowers you to design a secure app by implementing the principle of least privilege. Deep interaction with the web content not core to your app is disabled for both the code you write and any other code you might bring in from frameworks or libraries.
This affects me with Browser Pets. Comments on a post can have links to other domains to learn more about cats and dogs.
I want to allow these links, and I can do so safely as long as I limit my App-bound domains to WebKittens and Pups on Safari.
It would become impossible for user data to be accidentally compromised on these other domains where I don't have a deep understanding of their structure or function, or intentionally compromised if my users run across a bad actor on the Web. I don't necessarily know what nefarious plans the developers of this site might have.
Adopting it is easy. You just need to add an entry for WKAppBoundDomains to your app's info.Plist, and point it to an array of domains. Here's how I adopted it in Browser Pets. Loading any other domain still works, but deep interaction with other domains is prevented at a technological level.
It's even possible to disable deep interaction with all domains in your app by simply adding the key to your info.Plist with an empty set of values. If you load arbitrary content but don't need any interaction with the web content itself, this is a best practice.
So, that was our journey. We took two separate apps displaying content from two separate websites and integrated them into one news feed. And the morale of the Safari and WebKit teams has never been higher.
Along the way, we made sure our app's JavaScript was isolated from the web content itself. We leveraged callAsyncJavaScript and postMessageWithReplies to greatly simplify our JavaScript logic. We fine-tuned how the content displays using new APIs that affect rendering. We added an easy "find in page" and a powerful "share as PDF" feature as well as printing on the Mac.
And we respected our users' privacy out of the gate by embracing Intelligent Tracking Prevention and adopting App-bound domains. All of these features were driven directly by developer feedback. We know we haven't gotten to everyone's requests, but remember, we're not done. We can't wait for you to try out these new APIs and we genuinely want to keep hearing what else you need in WKWebView.
In addition to feedback with Apple, webkit.org has multiple ways to reach out, including a link to our Slack and to our many mailing lists. WebKit is very open source, so this is also where you can learn to check out and build as well as file bugs.
We're also on Twitter.
If you want to stay on top of new Web-facing features, Web Inspector enhancements and other goodies you might see in future releases, we usually update Safari technology preview every two weeks. Purple Safari deserves a home in your doc.
Thank you for watching, and I hope you have a great rest of WWDC 2020.
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.