Caching Web Service Response Data in iOS

Many iOS applications obtain data via HTTP web services that return JSON documents. For example, the following code uses the HTTP-RPC WSWebServiceProxy class to invoke a simple web service that returns a simulated list of users as JSON. The results are stored as a dictionary and presented in a table view:

class UserViewController: UITableViewController {
    var users: [[String: Any]]! = nil

    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            // Load the data from the server
            AppDelegate.serviceProxy.invoke("GET", path: "/users") { result, error in
                if (error == nil) {
                    self.users = result as? [[String: Any]]

                    self.tableView.reloadData()
                } 
            }
        }
    }

    ...
}

This works fine when both the device and the service are online, but it fails if either one is not. In some cases this may be acceptable, but other times it might be preferable to show the user the most recent response when more current data is not available.

To facilitate offline support, the response data must be cached. However, since writing to the file system is a potentially time-consuming operation, it should be done in the background to avoid blocking the main (UI) thread. Here, the data is written using an operation queue to ensure that access to it is serialized:

class UserViewController: UITableViewController {
    var users: [[String: Any]]! = nil

    var userCacheURL: URL?
    let userCacheQueue = OperationQueue()

    override func viewDidLoad() {
        super.viewDidLoad()

        ...

        if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
            userCacheURL = cacheURL.appendingPathComponent("users.json")
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            // Load the data from the server
            AppDelegate.serviceProxy.invoke("GET", path: "/users") { result, error in
                if (error == nil) {
                    self.users = result as? [[String: Any]]

                    self.tableView.reloadData()

                    // Write the response to the cache
                    if (self.userCacheURL != nil) {
                        self.userCacheQueue.addOperation() {
                            if let stream = OutputStream(url: self.userCacheURL!, append: false) {
                                stream.open()

                                JSONSerialization.writeJSONObject(result!, to: stream, options: [.prettyPrinted], error: nil)

                                stream.close()
                            }
                        }
                    }
                } 
            }
        }
    }

    ...
}

Finally, the data can be retrieved from the cache if the web service call fails. The data is read from the cache in the background, and the UI is updated by reloading the table view on the main thread:

class UserViewController: UITableViewController {
    var users: [[String: Any]]! = nil

    var userCacheURL: URL?
    let userCacheQueue = OperationQueue()

    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            // Load the data from the server
            AppDelegate.serviceProxy.invoke("GET", path: "/users") { result, error in
                if (error == nil) {
                    self.users = result as? [[String: Any]]

                    self.tableView.reloadData()

                    // Write the data to the cache
                    if (self.userCacheURL != nil) {
                        self.userCacheQueue.addOperation() {
                            if let stream = OutputStream(url: self.userCacheURL!, append: false) {
                                stream.open()

                                JSONSerialization.writeJSONObject(result!, to: stream, options: [.prettyPrinted], error: nil)

                                stream.close()
                            }
                        }
                    }
                } else if (self.userCacheURL != nil) {
                    // Read the data from the cache
                    self.userCacheQueue.addOperation() {
                        if let stream = InputStream(url: self.userCacheURL!) {
                            stream.open()

                            self.users = (try? JSONSerialization.jsonObject(with: stream, options: [])) as? [[String: Any]]

                            stream.close()
                        }

                        // Update the UI
                        OperationQueue.main.addOperation() {
                            self.tableView.reloadData()
                        }
                    }
                }
            }
        }
    }

    ...
}

Now, as long as the application has been able to connect to the server at least once, it can function either online or offline, using the cached response data.

For more ways to simplify iOS app development, please see my projects on GitHub:

Applying Style Sheets Client-Side in iOS

While native mobile applications can often provide a more seamless and engaging user experience than a browser-based app, it is occasionally convenient to present certain types of content using a web view. Specifically, any content that is primarily text-based and requires minimal user interaction may be a good candidate for presentation as HTML; for example, product descriptions, user reviews, or instructional content.

However, browser-based content often tends to look out of place within a native app. For example, consider the following simple HTML document:

<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="initial-scale=1.0"/>
</head>
<body>
    <h1>Lorem Ipsum</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    <ul>
    <li>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</li> 
    <li>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</li>
    <li>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</li>
    </ul>
</body>
</html>

Rendered by WKWebView, the result looks like this:

Because the text is displayed using the default browser font rather than the system font, it is immediately obvious that the content is not native. To make it appear more visually consistent with other elements of the user interface, a stylesheet could be used to render the document using the system font:

<head>
    ...
    <style>
    body {
        font-family: '-apple-system';
        font-size: 10pt;
    }
    </style>
</head>

The result is shown below. The styling of the text now matches the rest of the UI:

However, while this approach may work for this simple example, it does not scale well. Different app (or OS) versions may have different styling requirements.

By applying the stylesheet on the client, the presentation can be completely separated from the content. This can be accomplished by linking to the stylesheet rather than embedding it inline:

<head>
    ...
    <link rel="stylesheet" type="text/css" href="example.css"/>
</head>

However, instead of downloading the stylesheet along with the HTML document, it is distributed with the application itself and applied using the loadHTMLString:baseURL: method of the WKWebView class. The first argument to this method contains the (unstlyed) HTML content, and the second contains the base URL against which relative URLs in the document (such as stylesheets) will be resolved:

class ViewController: UIViewController {
    var webView: WKWebView!

    var serviceProxy: WSWebServiceProxy!

    override func loadView() {
        webView = WKWebView()

        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Local CSS Example"

        serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: URL(string: "http://localhost")!)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        serviceProxy.invoke("GET", path: "example.html") { result, error in
            if (result != nil) {
                self.webView.loadHTMLString(result as! String, baseURL: Bundle.main.resourceURL!)
            }
        }
    }
}

In this example, the document, example.html, is loaded using the HTTP-RPC WSWebServiceProxy class. The stylesheet, example.css, is stored in the resource folder of the application's main bundle:

body {
    font-family: '-apple-system';
    font-size: 10pt;
}

The results are identical to the previous example. However, the content and visual design are no longer tightly coupled and can vary independently:

For more ways to simplify iOS app development, please see my projects on GitHub: