A tale of a Cordova developer fighting with WebViews on iOS

A journey from the past to the future of WebViews in Apache Cordova for iOS

If you are reading this you probably are familiar with Apache Cordova and how it works. Just as a short refresher: Cordova is framework for building cross-platform apps using HTML, CSS and Javascript. Cordova works by leveraging the webviews available on mobile operating systems. A webview is basically a browser view embedded into a native app. The developer builds the apps user interface using web technology and bundles it with native code (in plugins) as native app for distribution in the app stores.

Once upon a time a junior developer was tasked with building an app using Cordova. He took over the codebase of an app and started working on it. The app worked great but sometimes he encountered strange rendering issues. After long investigations he found out these issues only happening on iOS because of this webview implementation called UIWebView.

I am this junior developer and this was the start of my journey with webviews on iOS. This work with webviews and Cordova on iOS kept me busy a few times in my job and got me to engage with Cordova and WebKit developers. To this day we still have strange problems and solutions for webview related problems on Cordova and I think having a look at the history may help finding a better solution for the future.

Ignore the memes if you want to take this post seriously. I just tried to spice it up a little since probably nobody would want to read this wall of text with just ramblings about webviews. To be honest working on this shaped my career in some way and I try to make app development better for many people out there using Cordova and similar technologies.

UIWebView

MEME: Remember UIWebView? Pepperidge Farm remembers.

UIWebview was the first webview option available to iOS developers since iOS 2.0. Cordovas iOS platform was built around using UIWebView for most of it’s history. UIWebView has one big advantage for us Cordova developers but many disadvantages. It’s is considered deprecated and should not be used in new apps.

CORS

UIWebView generally worked perfectly for many years. It doesn’t have cross-origin resource sharing (CORS) policies for local files like its successor. Because Cordova works by loading local files from the apps bundle into the WebView it was a very convenient to use by app developers. App developers did not have to care about CORS policies on servers they are interacting with. To me this totally makes sense because apps using a webview to load local web pages as part of the app are just like native apps. Native code does not have the CORS restrictions web pages have and Cordova apps in this context are just native apps.

CORS totally makes sense in a browser context where web pages from different origins need to be separated from each other, but the local pages in Cordova are something different.

MEME: UIWebViews loading of local files with no CORS restrictions was nice, Change my mind

Rendering issues and performance problems

MEME: Opening my app after the iOS update, visible confusion

UIWebView always had small rendering issues that required custom CSS and JavaScript to fix, if it was possible at all. Many hard to find bugs tortured developers. UIWebView was not as fast as its successor WKWebView, too. Especially with the iOS 12 update performance took a hit and some pages loaded many times slower.

Deprecation

MEME: We have to migrate to WKWebView quickly!, panic

Additionally to these problems Apple has been suggesting developers to move their apps to WKWebView. This meant I had to figure out how to migrate this app. Cordova had support for WKWebView for a while but it required some changes to app and servers.

A while after my work on this started Apple finally announced the deprecation of UIWebView in iOS 13 and that uploads to the App Store with UIWebView will be rejected. On the Cordova side this meant all UIWebView code in the iOS platform and plugins had to be removed. This happened in two stages. At first a WKWebViewOnly preference was introduced to exclude UIWebView code at compile time. This was released in a minor version to address the upload warnings developers now got for uploading to the App Store. After a while a new major version removed the UIWebView code and added the WKWebView implementation to the platform.

This was after I became Project Management Committee (PMC) member for Cordova and I was happy to help with releasing these versions and working on the InAppBrowser plugin. This post explains how the preference worked and the update post explains the changes in cordova-ios 6.x.

Early days of WKWebView

Cordova app developers had the option to use WKWebView for a long time with the official Apache WKWebView-Engine plugin. This plugin uses WKWebView instead of the built in UIWebView to load the local files of the Cordova app. Because WKWebView enforces CORS on the file: protocol it was not a drop-in replacement for many developers. Another problem of the plugin is that many developers use a web framework like Angular or React that use URL based routing which does not work properly for sites running on the file protocol.

Ionic the company behind the Ionic framework that traditionally uses Angular addressed this problem with their fork of the WKWebView plugin. Their version uses an iOS API called WKURLSchemeHandler to load the local files via a custom protocol like ionic://localhost. This solves the routing problems and this plugin is way more popular than the Apache one (according to NPM download stats).

These WKWebView options are just fine for many app developers and their environments. With cordova-ios 6.0.0 WKWebView came to the Cordova iOS platform with the schemehandler and UIWebView was removed. In most cases you should be happy now with everything Cordova now offers.

Intelligent Tracking Prevention (ITP) in iOS 13

When iOS 13 came into beta a wild ride ride began for me. I started testing the app with the beta not long before the official release and it did not work properly because of cookie issues. The app uses cookies for authentication and it was completely broken because WKWebView did not send cookies in our cross-origin setup. I opened a bug on the WebKit bug tracker and the discussion started what could be the source of the issue. I lived in fear for a while but the iOS 13.2 developer beta 2 finally fixed it. Cookies worked as they used to do again.

Nevertheless iOS 13 got me panic again when iOS 13.3 was released and cookies stopped working again. Again I opened a bug for WebKit and it got fixed really quickly in iOS 13.3 beta 4 and was resolved even before it got released.

I learned a lesson in this iOS 13 release that third party cookies are coming to an end. iOS 13 shipped some big privacy features for Safari which protect users from tracking with cookies. I personally really like the push for more privacy in browsers and making the live hard for “evil” tracking cookies. Unfortunately an app setup with the Cordova app running on one origin and the server running on a different one is pretty similar to most (ad) tracking technology.

From the comments in the WebKit bugs I learned that those bugs that troubled my app were regressions from changes for privacy features that just accidentally applied to WKWebView. WKWebView on iOS 13 was not supposed to enable these features, yet but I could feel they would become enabled in the future.

ITP rolling out in iOS 14

About a year later exactly this happened. The iOS 14 developer beta came out and I immediately noticed that cookies are blocked again. Changes to WKWebView where officially announced at the WWDC 2020 in this video. Basically WKWebView now enforces the tracking prevention by default and developers have to take some measures to adapt their apps.

Luckily I already found ways to make cookies and CORS work together in the mean time. More on that later in this post.

App-Bound Domains

The preferred way to properly setup WKWebView for secure communication with servers in iOS 14 is to configure app-bound domains. I really like the idea behind that and this is probably the best solution for the most apps out there. Cordova is already investigating to integrate that. Unfortunately for my personal use case this is not a solution since my app allows the user to configure it’s own server which means I don’t have a list of known domains to set at build time.

Hacks to make WKWebView work

MEME: Let’s make WKWebView work for me

For the migration to WKWebView I had to figure out solutions to the CORS and cookie problems mentioned before. With Cordova I had several plugins available that use native code to work around the limitations the webview now enforces.

Native XHR Plugin

My first idea was using plugins like cordova-plugin-wkwebview-file-xhr or cordova-plugin-advanced-http. These plugins basically allow you to do web requests through native code where no CORS restrictions apply.

I quickly came to the conclusion that these plugins were no ideal solution because they had the big drawback that the apps webview and InAppBrowser don’t sync the cookies and therefore are not authenticated with the server. This means loading some content from the server in InAppBrowser won’t work without the user logging in again.

Origin and hostname switching

The next idea was kind of interesting and was even used in production for some time. As a first step we brought WKWebView to production by checking if the server supports WKWebView and switch between UIWebView and WKWebView. I figured out how to switch Cordovas webview at runtime and built a plugin for that. You load the app in WKWebView, do a request and if it fails call JavaScript to tell Cordova to switch to UIWebView. It worked, but it was a pain to implement into the apps code and a really bad user experience because the app had to reload the page.

We still needed to get rid of UIWebView completely. I thought how can we solve all CORS and third party cookie issues? Just make requests first party!

MEME: Can’t have cross origin issues, if you are same origin

Again I tinkered a bit with Cordovas internals and found a way to set the hostname of the WebView. I added this as a new feature to the plugin I created before and you now could set the apps origin via JavaScript. Like many apps the app is using Ionic webview plugin which let’s the apps page run on a custom scheme like ionic://localhost. If the user configures the app to use theirserver.com the app changes the hostname with this WebviewSwitch.setHostname('theirserver.com') and it will reload and run with the origin ionic://theirserver.com. This will treat HTTP requests to theirserver.com as same-origin with no CORS and cookie restrictions.

WKURLSchemeHandler Proxy

These solutions worked in production for a while but were not perfect. In particular the user experience was bad because the app had to reload in some cases which slowed it down significantly at start and bothered the user with white pages. While discussing this issue with some people in the Cordova community a fellow PMC member had an idea I just needed to try out.

As mentioned a few times the Ionic webview plugin uses WKURLSchemeHandler introduced in iOS 11 to load content from the app with a custom URL scheme. Why not use this scheme to load stuff from the internet, too? This means we could rewrite all URLs to the custom scheme and use native code to make the request. CORS and tracking prevention would not apply because to the webview this request is same origin. This sounds great right?

After a bit trial and error I found a way to expands Ionics WKURLSchemeHandler to do internet requests as well. After rewriting all URLs in the app with a helper function I tell XHR, fetch and even image tags to go through my now dubbed “proxy” WKURLSchemeHandler. This proved to be a good solution.

Future in Cordova

Since cordova-ios 6.0 WKWebView is the default and it uses WKURLSchemeHandler, too. This means I could get rid of the Ionic webview for iOS. I thought about adding my “proxy” to the iOS platform and started the discussion with other Cordova maintainers. After a year of using this in a fork I finally did the pull request to the iOS platform. This is still in an early stage (for example the Cordova whitelist is not considered, yet) but I hope to bring this feature to a future release. Time will tell if this solution will be part of Cordova and still work in the future.

Some ideas for WebKit

This post sums up my turbulent history with WKWebView. In the past major iOS updates really caused a lot of work to keep the app working and caused a lot of uncertainty. Most of this was because of the outdated and strange use of cookies for authentication. By now many apps are using modern solutions like OAuth and tokens and developers really should move to modern solutions if they can. The ice is getting thinner for cookies. Tracking cookies are getting blocked and this is great. Functional cookies might get affected unwittingly and we need to find ways to fix this. Some apps just need to work with external services that need cookies for authentication and cannot be changed.

As far as I know a large portion of iOS apps out there are hybrid apps built with Cordova or similar technology. Small changes or new features to WebKit could potentially improve a large numbers of apps. WKURLSchemeHandler has been a blessing for WKWebView in Cordova. The “proxy” currently is a good solution. but does not really feel right. I personally still feel the uncertainty with every iOS update that this may break in another strange way. Let’s work together between app/framework developers and WebKit to make lives easier for developers and apps more reliable. I got some ideas but finding the right balance between privacy, security and features for developers is really hard.