Check network in UIWebView app

image

Use Swift to handle online/offline states of your UIWebView wrapped HTML5 app.

Background

There are many HTML5 frameworks that help build web apps that look and behave much like native mobile apps. Ionic and Framework7 are a couple of my favorites for building Ruby apps that look like native iOS. On iOS, these web apps can then be wrapped and run in a UIWebView within a native iOS app. While many apps in the App Store are built this way, you do need to do some legwork beyond loading your web app URL inside the web view before you can submit it to the App Store.

For example, Apple will immediately reject apps that doesn’t handle online/offline detection. This post outlines the approach I take to handle this when I wrap my Ruby/Rails/Framework7 powered apps inside a UIWebView.

I assume that you

  • Know some basic Swift, so that you know what I talk about when I refer to implementing a protocol, ask to be a delegate, etc.
  • Have already added a UIWebView to your Storyboard, and have created an IBOutlet to wire it up to your ViewController. I call my IBOutlet browser, which you will see referenced.
  • Have already created an HTML file that will be displayed when we don’t have a network connection. I’ve called mine offline.html, which you will see referenced.

Overview

These are the functions we’ll create:

  • checkNetwork
    • Checks whether we are online or offline, and stores the result for future use.
  • loadWebApp
    • Loads the application website URL in UIWebView
  • displayOffline
    • Displays the offline HTML in UIWebView

We want to run checkNetwork on several occasions:

  • On app launch
  • When app comes to foreground, if last result was ”offline”
  • Every few seconds while app is offline and in foreground
  • When UIWebView fails to load a page

We will be a little more conservative with calling loadWebApp and displayOffline, because if we for example call loadWebApp every time the app comes in to focus, the whole app would reload and the user’s present position in the app would be lost.

Add Reach.swift to project

I use Garfias Lopez’s excellent Reach.swift to perform the network detection. It lets you read current network status by simply calling Reach().connectionStatus(). Create a new Swift file named Reach.swift and paste the code in place.

NetworkStatus.swift

I like to use structs to store data that I want to keep readily available. So add a struct to store last status that Reach returned. Choose File > New > File... > Swift File, and create a new file NetworkStatus.swift:

NetworkStatus.swift
import Foundation

struct NetworkStatus {
    static var lastStatus: ReachabilityStatus = .unknown
}

We will use NetworkStatus.lastStatus in a bit. Sit tight.

ReachabilityStatus is an enum provided by Reach, and can be one of .unknown, .online, and .offline. We’ll start with unknown until we’ve launched the app and performed the first checkNetwork.

First step is to check network status and store the result in the NetworkStatus struct. Add checkNetwork to the ViewController that contains your UIWebView:

ViewController.swift
func checkNetwork() {
    let status = Reach().connectionStatus()
    NetworkStatus.lastStatus = status
}

Just like that, last checked status will be neatly tucked away for when we need it.

Browser.swift

Your web view is initialized when the app launches, however if the app loses focus, and you bring the app back to the foreground, the app may have lost the context of it, which would lead to errors when you try to access it. Luckily for you, I did the cursing and wasting of hours in your stead, and we’ll simply go ahead and associate the web view to another struct, that we know will always be available. File > New > File... > Swift File > Browser.swift:

Browser.swift
import Foundation
import UIKit

struct Browser {
    static var webview: UIWebView?
}

Then, in ViewController’s viewDidLoad, add:

ViewController.swift
override func viewDidLoad() {
    ...
    browser.delegate = self
    Browser.webview = browser
}

Starting with the second line, it associates the web view with our new Browser struct, so that instead of doing browser.loadRequest(request), which may throw an error after the app has been in the background, we can call Browser.webview.loadRequest(request), since the Browser struct won’t lose its context once it’s been initialized.

Going back to the first line, we’re getting ahead of ourselves a little by setting the delegate to self. The reason we do it is because we will soon implement a method of the UIWebViewDelegate protocol, and this is telling the web view that we want to be its delegate. Since we’re stating that we want to be the delegate for the UIWebViewDelegate protocol, we also need to add UIWebViewDelegate to the class definition:

ViewController.swift
class ViewController: UIViewController, UIWebViewDelegate {
  ...
}

We’ll get back to the delegate protocol method in a bit.

loadWebViewFromURL

I only mentioned displayOffline and loadWebApp before, but we’ll also create a function called loadWebViewFromURL that will be used to avoid code duplication. Add this to your ViewController:

ViewController.swift
func loadWebViewFromURL(_ url: URL) {
    let request = URLRequest(url: url)
    Browser.webview?.loadRequest(request)
}

func displayOffline() {
    let url = Bundle.main.url(forResource: "offline", withExtension: "html")
    loadWebViewFromURL(url!)
}

func loadWebApp() {
    let url = URL(string: "http://your-web-app.com")
    loadWebViewFromURL(url!)
}

Checking on app launch

Alright, now we’re ready to dive in to the nitty gritty. We want to check network on app launch, which will store the result in our NetworkStatus struct and call either loadWebApp or displayOffline based on the result. We’ll put that in a new function checkNLoad that we call from viewDidLoad:

ViewController.swift
func checkNLoad() {
    checkNetwork()
    switch NetworkStatus.lastStatus {
    case .unknown, .offline:
        displayOffline()
    default:
        loadWebApp()
    }
}

override func viewDidLoad() {
    ...
    browser.delegate = self
    Browser.webview = browser
    checkNLoad()
}

Returning to foreground

Next up, we want to call checkNLoad whenever the app returns to the foreground after having lost focus, but only if it was offline last time we checked. That way, the user won’t lose their position if they were merrily going about their business last time the app was in the foreground. To do this, we’ll head over to AppDelegate.swift and add this to applicationWillEnterForeground:

AppDelegate.swift
func applicationWillEnterForeground(_ application: UIApplication) {
    ...
    if case .offline = NetworkStatus.lastStatus {
        ViewController().checkNLoad()
    }
}

When UIWebView fails to load a page

We don’t want to set a timer to continuously check if we are online, since that kind of brute force would waste data, and because we don’t want to alert the user if there’s merely a momentary blip in the connection, since chances are they are just reading a page and not requesting network right that moment. We’ll tell our users on a need to know basis.

This is where we’ll implement one of the UIWebViewDelegate protocol methods. The one we want is didFailLoadWithError, that is called if the web view fails to load expected content. If that happens, we’ll check if we’re offline. If we are, we’ll call displayOffline. To do this, add to ViewController:

ViewController.swift
func webView(_: UIWebView, didFailLoadWithError: Error) {
    checkNetwork()
    if case .offline = NetworkStatus.lastStatus {
        displayOffline()
    }
}

NOTE: didFailLoadWithError is only triggered by a full page load, not by JavaScript AJAX calls that fetches and replaces merely a part of the DOM tree on a page. If you rely heavily on AJAX, you may want to add callbacks to your AJAX requests that can display a warning in case it takes longer than expected to return a response.

Getting back online

There’s one exception though when we do want to set a timer to continuously check whether or not we’re online. That is while we have no connection. Then we want to check every few seconds to see if we’ve gotten our signal back, and if so, we’ll call loadWebApp to reload the app into the web view.

Let’s use our NetworkStatus struct to handle this. Add function fireRepeatedOnlineCheck to create a timer that fires every 5 seconds and will check whether we’re back online. If so, good ’ol loadWebApp is called. We also create an optional variable timer to hold the timer that fireRepeatedOnlineCheck initializes.

Its counterpart invalidateRepeatedOnlineCheck simply invalidates any timer that may or may not exist in the optional variable timer.

NetworkStatus.swift
struct NetworkStatus {
    static var lastStatus: ReachabilityStatus = .unknown
    static var timer: Timer?

    static func fireRepeatedOnlineCheck() {
        NetworkStatus.timer = Timer.scheduledTimer(withTimeInterval: 5.0, 
          repeats: true, block: {(Void)  in
            ViewController().checkNetwork()
            if case .online = NetworkStatus.lastStatus {
                ViewController().loadWebApp()
            }
        })
    }

    static func invalidateRepeatedOnlineCheck() {
        NetworkStatus.timer?.invalidate()
    }
}

To trigger fireRepeatedOnlineCheck and invalidateRepeatedOnlineCheck respectively, we add calls to them in displayOffline and loadWebApp to fire the timer when offline.html is loaded, and to invalidate and remove the timer when we’re back online:

ViewController.swift
func displayOffline() {
    NetworkStatus.fireRepeatedOnlineCheck()
    let url = Bundle.main.url(forResource: "offline", withExtension: "html")
    loadWebViewFromURL(url!)
}

func loadWebApp() {
    NetworkStatus.invalidateRepeatedOnlineCheck()
    let url = URL(string: "http://your-web-app.com")
    loadWebViewFromURL(url!)
}

Finally, we’ll add a call to invalidateRepeatedOnlineCheck when our app is about to lose foreground focus. Open AppDelegate.swift, and add to applicationWillResignActive:

AppDelegate.swift
func applicationWillResignActive(_ application: UIApplication) {
    ...
    NetworkStatus.invalidateRepeatedOnlineCheck();
}

Conclusion

Thats it! If this post was helpful, let me know in the comments section. None of this is really difficult, it’s mostly just a lot of little things to think about.

UPDATE: I’ve created a demo project for Xcode 8 and Swift 3 that you can download if you want to see all these pieces in action.