Dynamically Loading Table View Images in iOS

iOS applications often display thumbnail images in table views alongside other text-based content such as contact names or product descriptions. However, these images are not usually delivered with the initial request, but must instead be retrieved separately afterward. They are typically downloaded in the background as needed to avoid blocking the main thread, which would temporarily render the user interface unresponsive.

For example, consider this REST service, which returns a list of simulated photo data:

[
  {
    "albumId": 1,
    "id": 1,
    "title": "accusamus beatae ad facilis cum similique qui sunt",
    "url": "http://placehold.it/600/92c952",
    "thumbnailUrl": "http://placehold.it/150/92c952"
  },
  {
    "albumId": 1,
    "id": 2,
    "title": "reprehenderit est deserunt velit ipsam",
    "url": "http://placehold.it/600/771796",
    "thumbnailUrl": "http://placehold.it/150/771796"
  },
  {
    "albumId": 1,
    "id": 3,
    "title": "officia porro iure quia iusto qui ipsa ut modi",
    "url": "http://placehold.it/600/24f355",
    "thumbnailUrl": "http://placehold.it/150/24f355"
  },
  ...
]

Each record contains a photo ID, album ID, and title, as well as URLs for both thumbnail and full-size images; for example:

Photo Class

A class representing this data might be defined as follows:

class Photo: NSObject {
    var id: Int = 0
    var albumId: Int = 0
    var title: String?
    var url: URL?
    var thumbnailUrl: URL?

    init(dictionary: [String: Any]) {
        super.init()

        setValuesForKeys(dictionary)
    }

    override func setValue(_ value: Any?, forKey key: String) {
        switch key {
        case #keyPath(url):
            url = URL(string: value as! String)

        case #keyPath(thumbnailUrl):
            thumbnailUrl = URL(string: value as! String)

        default:
            super.setValue(value, forKey: key)
        }
    }
}

Since instances of this class will be populated using dictionary values returned by the web service, an initializer that takes a dictionary argument is provided. The setValuesForKeys(_:) method of NSObject is used to map dictionary entries to property values. The ID and title properties are handled automatically by this method; the class overrides setValue(_:forKey:) to convert the URLs from strings into actual URL instances.

View Controller Class

A basic user interface for displaying service results in a table view is shown below:

Row data is stored in an array of Photo instances. Previously loaded thumbnail images are stored in a dictionary that associates UIImage instances with photo IDs:

class ViewController: UITableViewController {
    // Row data
    var photos: [Photo]!

    // Image cache
    var thumbnailImages: [Int: UIImage] = [:]

    ...    
}

The photo list is loaded the first time the view appears. The WSWebServiceProxy class of the open-source HTTP-RPC library is used to retrieve the data. If the call succeeds, the response data (an array of dictionary objects) is transformed into an array of Photo objects and the table view is refreshed. Otherwise, an error message is displayed:

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

    // Load photo data
    if (photos == nil) {
        let serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: URL(string: "https://jsonplaceholder.typicode.com")!)

        serviceProxy.invoke("GET", path: "/photos") { result, error in
            if (error == nil) {
                let photos = result as! [[String: Any]]

                self.photos = photos.map({
                    return Photo(dictionary: $0)
                })

                self.tableView.reloadData()
            } else {
                let alertController = UIAlertController(title: "Error", message: error!.localizedDescription, preferredStyle: .alert)

                alertController.addAction(UIAlertAction(title: "OK", style: .default))

                self.present(alertController, animated: true)
            }
        }
    }
}

Cell content is generated as follows. The corresponding Photo instance is retrieved from the photos array and used to configure the cell:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return (photos == nil) ? 0 : photos.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)

    if (cell == nil) {
        cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
    }

    let photo = photos[indexPath.row];

    cell!.textLabel!.text = photo.title

    // Attempt to load image from cache
    cell!.imageView!.image = thumbnailImages[photo.id]

    if (cell!.imageView!.image == nil && photo.thumbnailUrl != nil) {
        // Image was not found in cache; load it from the server

        ...
    }

    return cell!
}

If the thumbnail image is already available in the cache, it is used to populate the cell's image view. Otherwise, it is loaded from the server and added to the cache as shown below. If the cell is still visible when the image request returns, it is updated immediately and reloaded:

let serverURL = URL(string: String(format: "%@://%@", photo.thumbnailUrl!.scheme!, photo.thumbnailUrl!.host!))!

let serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: serverURL)

serviceProxy.invoke("GET", path: photo.thumbnailUrl!.path) { result, error in
    if (error == nil) {
        // If cell is still visible, update image and reload row
        let cell = tableView.cellForRow(at: indexPath)

        if (cell != nil) {
            if let thumbnailImage = result as? UIImage {
                self.thumbnailImages[photo.id] = thumbnailImage

                cell!.imageView!.image = thumbnailImage

                tableView.reloadRows(at: [indexPath], with: .none)
            }
        }
    } else {
        print(error!.localizedDescription)
    }
}

Finally, if the system is running low on memory, the image cache is cleared:

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()

    // Clear image cache
    thumbnailImages.removeAll()
}

Summary

This article provided an overview of how images can be dynamically loaded as needed to populate table view cells in iOS. For more ways to simplify iOS app development, please see my projects on GitHub:

  • MarkupKit – declarative UI for iOS and tvOS
  • HTTP-RPC – lightweight multi-platform REST

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:

  • MarkupKit – Declarative UI for iOS and tvOS
  • HTTP-RPC – Lightweight multi-platform REST

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:

  • MarkupKit – Declarative UI for iOS and tvOS
  • HTTP-RPC – Lightweight multi-platform REST

MarkupKit 3.2 Released

MarkupKit 3.2 is now available for download. Among other minor enhancements and fixes, this release adds declarative support for UITabBar:

<UITabBar id="tabBar">
    <item type="featured" name="featured"/>
    <item type="recents" name="recents"/>
    <item type="bookmarks" name="bookmarks"/>
    <item type="search" name="search"/>
    <item type="downloads" name="downloads"/>
</UITabBar>

For more information, see the project README.

Natively Submitting HTML Forms in iOS

HTML forms are a common means of uploading information to a web server. For example, the HTML 5 specification includes a sample form that simulates a pizza delivery request. The form allows the user to provide contact information, pizza size and topping details, and delivery instructions.

A working implementation of this form can be found on httpbin.org. Submitting the form produces a JSON response that simply echoes the information sent with the request. However, in most "real world" applications, a form submission typically triggers some meaningful action on the server, such as posting a message, responding to a survey, or making a purchase.

Mobile applications often need to perform similar tasks. While it is possible to embed an HTML form in a native application using a web view, this is not always ideal. For example, the form may not be optimized for a mobile device, resulting in controls that are too small and difficult to interact with. Further, embedded forms don't generally provide the seamless experience users expect from a native application. They often look out of place, making it obvious that the app is not truly native:

A form constructed with native controls is usually more visually consistent with platform conventions and much easier to interact with. For example:

However, many native forms are processed via some form of custom XML or JSON-based web service API. This represents a duplication of effort, since developers need to support both the form processing code as well as the code for implementing the web service. It would be ideal if the server-side logic could be shared by both clients, reducing overall development effort while preserving the enhanced user experience provided by the native UI.

MarkupKit

MarkupKit is an open-source framework for simplifying development of native iOS and tvOS applications. It allows developers to construct user interfaces declaratively using a human-readable, HTML-like markup language, rather than visually using Interface Builder or programmatically in code.

For example, the following markup creates an instance of UILabel and sets the value of its text property to "Hello, World!":

<UILabel text="Hello, World!"/>

This markup is equivalent to the following Swift code:

let label = UILabel()
label.text = "Hello, World!"

The native form shown in the previous section was created using the following markup. It declares a static table view whose contents represent the elements of the delivery request form:

<LMTableView style="groupedTableView">
    <!-- Contact information -->
    <LMTableViewCell selectionStyle="none">
        <UITextField id="nameTextField" placeholder="Customer Name"/>
    </LMTableViewCell>

    <LMTableViewCell selectionStyle="none">
        <UITextField id="phoneTextField" placeholder="Telephone" keyboardType="numberPad"/>
    </LMTableViewCell>

    <LMTableViewCell selectionStyle="none">
        <UITextField id="emailTextField" placeholder="Email Address" keyboardType="emailAddress"/>
    </LMTableViewCell>

    <!-- Pizza size/toppings -->
    <?sectionBreak?>
    <?sectionName size?>
    <?sectionSelectionMode singleCheckmark?>

    <sectionHeader title="Pizza Size"/>

    <UITableViewCell textLabel.text="Small" value="small"/>
    <UITableViewCell textLabel.text="Medium" value="medium"/>
    <UITableViewCell textLabel.text="Large" value="large"/>

    <?sectionBreak?>
    <?sectionName toppings?>
    <?sectionSelectionMode multipleCheckmarks?>

    <sectionHeader title="Pizza Toppings"/>

    <UITableViewCell textLabel.text="Bacon" value="bacon"/>
    <UITableViewCell textLabel.text="Extra Cheese" value="cheese"/>
    <UITableViewCell textLabel.text="Onion" value="onion"/>
    <UITableViewCell textLabel.text="Mushroom" value="mushroom"/>

    <!-- Delivery time/instructions -->
    <?sectionBreak?>

    <sectionHeader title="Preferred Delivery Time"/>

    <LMTableViewCell selectionStyle="none">
        <UIDatePicker id="deliveryDatePicker" datePickerMode="time" height="140"/>
    </LMTableViewCell>

    <?sectionBreak?>

    <sectionHeader title="Delivery Instructions"/>

    <LMTableViewCell selectionStyle="none">
        <UITextView id="commentsTextView" height="140" textContainerInset="4" textContainer.lineFragmentPadding="0"/>
    </LMTableViewCell>
</LMTableView>

Of course, MarkupKit isn't the only way to create native forms in iOS; however, it can significantly simplify the task.

HTTP-RPC

HTTP-RPC is an open-source framework for simplifying development of REST applications. It allows developers to access REST-based web services using a convenient, RPC-like metaphor while preserving fundamental REST principles such as statelessness and uniform resource access.

The project currently includes support for consuming web services in Objective-C/Swift and Java (including Android). It provides a consistent, callback-based API that makes it easy to interact with services regardless of target device or operating system.

For example, the following code snippet shows how a Swift client might access a simple web service that returns a friendly greeting:

Swift

serviceProxy.invoke("GET", path: "/hello") { result, error in
    print(result) // Prints "Hello, World!"
}

While HTTP-RPC is often used to access JSON-based REST APIs, it also supports posting data to the server using the application/x-www-form-urlencoded MIME type used by HTML forms.

For example, the following view controller uses the iOS HTTP-RPC client to submit the contents of the form from the previous section to the test service at httpbin.org. The actual form submission is performed in the submit() method using HTTP-RPC's WSWebServiceProxy class:

class ViewController: LMTableViewController {
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var phoneTextField: UITextField!
    @IBOutlet var emailTextField: UITextField!
    @IBOutlet var deliveryDatePicker: UIDatePicker!
    @IBOutlet var commentsTextView: UITextView!

    override func loadView() {
        view = LMViewBuilder.view(withName: "ViewController", owner: self, root: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Pizza Delivery Form"

        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit", style: UIBarButtonItemStyle.plain,
            target: self, action: #selector(submit))

        deliveryDatePicker.minuteInterval = 15
    }

    func submit() {
        let timeFormatter = DateFormatter()

        timeFormatter.dateFormat = "hh:mm"

        let serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: URL(string: "https://httpbin.org")!)

        serviceProxy.encoding = WSApplicationXWWWFormURLEncoded

        serviceProxy.invoke("POST", path: "/post", arguments: [
            "custname": nameTextField.text ?? "",
            "custtel": phoneTextField.text ?? "",
            "custemail": emailTextField.text ?? "",
            "size": tableView.value(forSection: tableView.section(withName: "size")) ?? "",
            "topping": tableView.values(forSection: tableView.section(withName: "toppings")),
            "delivery": timeFormatter.string(from: deliveryDatePicker.date),
            "comments": commentsTextView.text
        ]) { result, error in
            let alertController = UIAlertController(title: "Status",
                message: (error == nil) ? "Form submitted." : error!.localizedDescription,
                preferredStyle: .alert)

            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler:nil))

            self.present(alertController, animated: true, completion: nil)
        }
    }
}

While the example controller simply displays a success or failure message in response to the form submission, an actual application might do something slightly more sophisticated, such as presenting a confirmation page returned by the server.

As with MarkupKit, HTTP-RPC isn't strictly required, but its built-in support for executing URL-encoded form posts makes it a good option.

Summary

This article provided an overview of how MarkupKit and HTTP-RPC can be used to natively submit HTML forms in iOS, reducing development effort and improving user experience.

For more information, please see the following:

MarkupKit 3.1 Released

MarkupKit 3.1 is now available for download. This release improves selection management in table and picker views by allowing callers to get and set selection state by value, similar to a <select> element in HTML.

For example, a web page that allows a user to choose one of several size options might include something like the following:

<select name="size">
    <option value="S">Small</option>
    <option value="M">Medium</option>
    <option value="L">Large</option>
    <option value="XL">Extra-Large</option>
</select>

Alternatively, the options could be presented using radio buttons like this:

<input type="radio" name="size" value="S"/>Small<br/>
<input type="radio" name="size" value="M"/>Medium<br/>
<input type="radio" name="size" value="L"/>Large<br/>
<input type="radio" name="size" value="XL"/>Extra-Large<br/>

In MarkupKit, this can be implemented using LMTableView as shown below:

<LMTableView style="groupedTableView">
    <?sectionName sizes?>
    <?sectionSelectionMode singleCheckmark?>
    <UITableViewCell textLabel.text="Small" value="S"/>
    <UITableViewCell textLabel.text="Medium" value="M"/>
    <UITableViewCell textLabel.text="Large" value="L"/>
    <UITableViewCell textLabel.text="Extra-Large" value="XL"/>
    ...
</LMTableView>

The value associated with the currently selected row can be retrieved using the valueForSection: method of LMTableView. This method takes a single argument containing the section index, which can be obtained via the sectionWithName: method. Rows can be programmatically selected using setValue:forSection::

let value = tableView.value(forSection: tableView.section(withName: "sizes")) as! String

tableView.setValue("L", forSection: tableView.section(withName: "sizes"))

Picker Views

Alternatively, size selection could be implemented using a picker view:

<LMPickerView id="pickerView">
    <?componentName sizes?>
    <row title="Small" value="S"/>
    <row title="Medium" value="M"/>
    <row title="Large" value="L"/>
    <row title="Extra-Large" value="XL"/>
</LMPickerView>

In this case, the value associated with the selected row can be retrieved using the valueForComponent: method of LMPickerView, and rows can be programmatically selected using setValue:forComponent:. The component index can be obtained via componentWithName::

let value = pickerView.value(forComponent: pickerView.component(withName: "sizes")) as! String

pickerView.setValue("L", forComponent: pickerView.component(withName: "sizes"), animated: false)

Multiple Selection

In HTML, multiple options are often presented using a multi-select list element as shown below:

<select name="pets" size="5" multiple>
    <option value="D">Dog</option>
    <option value="C">Cat</option>
    <option value="F">Fish</option>
    <option value="R">Rabbit</option>
    <option value="T">Turtle</option>
</select>

Alternatively, the options may be represented by a group of checkboxes:

<input type="checkbox" name="pets" value="D"/>Dog<br/>
<input type="checkbox" name="pets" value="C"/>Cat<br/>
<input type="checkbox" name="pets" value="F"/>Fish<br/>
<input type="checkbox" name="pets" value="R"/>Rabbit<br/>
<input type="checkbox" name="pets" value="R"/>Turtle<br/>

In MarkupKit, this can also be implemented using LMTableView:

<LMTableView style="groupedTableView">
    <?sectionName pets?>
    <?sectionSelectionMode multipleCheckmarks?>
    <UITableViewCell textLabel.text="Dog" value="D"/>
    <UITableViewCell textLabel.text="Cat" value="C"/>
    <UITableViewCell textLabel.text="Fish" value="F"/>
    <UITableViewCell textLabel.text="Rabbit" value="R"/>
    <UITableViewCell textLabel.text="Turtle" value="T"/>
    ...
</LMTableView>

The values associated with the selected rows can be obtained using the valuesForSection: method, and set using setValues:forSection::

let values = tableView.values(forSection: tableView.section(withName: "pets")) as! [String]

tableView.setValues(["C", "T"], forSection: tableView.section(withName: "pets"))

Additional Information

For more information, please see the project README.