Bringing your web app to iOS
For many people, writing apps for iOS is something of a mystery. The two primary languages you'll be using (Swift and Objective-C) have very little use outside of Apple's ecosystem, and requires a lot of platform-specific learning to really master.
I was recently tasked with doing this for a friend, and I thought I would share my experience, as well as compile a list of tips and tricks for anyone else who might be going down this road. This isn't a step-by-step tutorial, but rather a set of guiding principles and useful APIs that might make your journey a little easier, as well as help you create a better product.
Reasons to build a container app
- You're still tinkering with your app's UI, and you want to be able to propagate changes to mobile easily.
- You plan on writing a fully native iOS app eventually, but you want something you can ship today.
Reasons to use React Native instead
You want the flexibility to re-use portions your code base for Android.
Basic First Steps
Before you start building your iOS container app, there's some scaffolding work you've gotta do
- Make sure your web app is built responsively, and works well on Mobile Safari.
- Register for the Apple Developer Program ($99/yr), and make sure you've got a unique bundle identifier and iTunes Connect app name that isn't already in use by someone else.
- Learn the basics of either Objective-C or Swift.
The idea is pretty simple. Create an instance of WKWebView, and use AutoLayout to make sure it takes up the whole screen, regardless of what device is being used. Create an URLRequest or NSURLRequest object, and load it up in the WebView. Getting this set up shouldn't take a whole lot of time, but if you want to make sure your users have a good experience, you aren't quite done yet.
Handling The URL Request Failures
Your web application probably uses a mix of actual page redirections, along with internally used XHR requests to either communicate with some API or load some resources. Now, your web app should already be built to handle failures in the latter scenario, but your web app typically relies on the web browser to display an error message when an HTTP request fails. You'll need to build that functionality into your web app by doing the following:
You'll need to declare conformity to WKNavigationDelegate, and implement the following methods
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error; - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error;
optional func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) optional func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
Sometimes, we know a request is going to fail even before it begins. If a device doesn't have an internet connection, why bother to make a request, only to have it time out and fail eventually? Your app can better handle changes in network connectivity using reachability monitoring. There are several ways to implement this. Apple provides a bare bones implementation themselves written in Objective-C (though, without ARC and other modern Objective-C standards). You can easily update this implementation and/or re-write it in Swift, or you can take advantage of the numerous projects on GitHub that have already done this for you. (Shameless plug, I've written one in Objective-C that you can use, available here)
Network reachability observation isn't required — you can easily rely on WKWebView requests to fail and inform the delegate when there isn't an active internet connection — but reachability observation creates a better experience for your users. The device can more quickly react to changes in network status, and you can provide contextual error messages like "You Have No Internet" vs. "The App Can't Be Reached Right Now"
Handling Loading Screens
WKWebView shows up as a blank, white canvas when it's in the middle of loading content. That's not what you want. At worst you'll confuse users who think it's a bug and at best it looks pretty ugly, especially when compared to your beautifully manicured web application. To solve this problem, we'll again take advantage of delegate messages sent from your WKWebView. Implement the following two methods in whatever object is serving as your WKNavigationDelegate:
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation; - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
optional func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) optional func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
These methods work exactly as you might expect. The the first method is called when the web view begins loading a page, and the second one is called when the web view finishes that page load.
Here, you can show / hide the web view as applicable, and show the user a loading indicator, perhaps using an UIActivityIndicatorView.
Authentication & Persistent Storage
A lot of web applications might have some kind of user authentication system, and that system is typically managed by some sort of token. Depending on how your web app is designed, you'll need to account for different authentication patterns in different ways. There isn't really a formula for doing this property, but here's some useful information that will help you make this important architectural decision:
- WKWebView doesn't share persistent storage with Safari. If a user is already logged in in mobile safari, they'll need to log-in again.
- WKWebView persists storage the same way Safari does. If your user needs to log in again every 30 days in Safari, for example, you can expect the same to be true here.
- You can view and delete persistent storage records in WKWebView. You can't add your own though.
- You can intercept page reloads and add additional info to the request like custom headers or body data. You can't intercept XHR requests.
- You can give your web app access to keychain data using Associated Domains. The user will still have to log in, but they might not have to remember their login credentials.
- You can specify a user agent that WKWebView should use instead of its default, and have your web app behave differently as necessary.
- If you prefer to do the login process outside of WKWebView, you can build that UI natively, and make requests to your API using NSURLSession (Objective-C) or URLSession (Swift)
Non-App Web Content
If your app links to content that doesn't easily link back to the app (i.e. a social media page), you'll want to intercept that traffic and open that link in Safari instead. Even though your web app functions like a web browser, it doesn't have any navigation controls, and your user will be stuck outside of your app until they force close and relaunch it. You can prevent this behavior and monitor requests using the navigation delegate, by implementing the following method:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
optional func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
When this method is implemented, you can check every navigation request before its executed and give it either allow or a deny. You can deny request that you don't want opened in the WKWebView, and use -openURL:options:completionHandler: method to open those request in Safari instead. Additionally, you can open up a mini version of Safari within your app using SFSafariViewController.
Additionally, you'll want to disable "peek" and "pop" 3D Touch gestures within WKWebView, which will brake the illusion and open your web app in Safari.
Do your best to make sure the bits of your UI that are rendered natively look and behave the same as their web app counter parts. A few easy ways to do this are:
- Make sure your launch image looks identical to the first page of your web app. You can re-create the UI using interface builder, or with a screenshot.
- Make sure the background color of the view controller where the web view lives matches the background color of your web app if possible, or the background color of your web app's loading screen if it has one.
- Make sure you're using the appropriate status bar style that best fits your web app's UI.
- If you're web app has a custom loading indicator, try to re-create that same control natively when your web view is loading content.
Dynamic Configuration / Testing
Depending on how long you plan on using your container app, make sure you've built a something that you can easily use for testing. Now that future releases of your web app need to be tested not only in mobile safari, but also in your container app, you'll want to be able to easily switch between production and staging environments as necessary. You'll also want to make sure you've explicitly set your URLRequest object's caching policy to ignore the local cache, so you can be sure that your container app is behaving correctly.
Pointing Mobile Safari Users To Your App
Once you're up an running, you'll probably want users to switch over from using safari to your native app. There are a couple of ways to do this
- For users who've yet to install the native app, you can setup your web app to display a Smart App Banner in Mobile Safari. (Don't worry, this banner won't show up in WKWebView).
- For users who've already installed the native app, use iOS Universal Links to automatically open links to your mobile web app in your native app instead of Safari.
Native Apps Are Still Better
Make no mistake about this. React Native and Mobile Container Apps are less than ideal solutions. Neither will give you the same level of performance or operating system level integration as a true, native iOS app.
For a lot of people though, an iOS web app container is a more than a good enough first step. The App Store is a great distribution platform for small independent developers to get their products some visibility in the marketplace, and getting a dedicated icon on a user's home screen is an easy way to maintain some basic retention on mobile. With enough tinkering & testing, you can easily create a iOS web app container that works well for most users, and effectively blurs the line between a native app and web app.
Check out an example on my GitHub page, if you're curious about how someone might do this in more detail. Note that every web app is different, and that you should really build around the specific needs of that web app if you really want the experience to be perfect.