Simplifying Auto Layout in iOS with MarkupKit

Auto layout is an iOS feature that allows developers to create “responsive” user interfaces; i.e., applications that automatically adapt to device size, orientation, or content changes. An application built using auto layout generally has no hard-coded view positioning logic, but instead dynamically arranges user interface elements based on their preferred or “intrinsic” content sizes.

Auto layout in iOS is implemented primarily via layout constraints, which, while powerful, are not particularly convenient to work with. This article provides an overview of how constraints are typically managed in an iOS application, and then discusses some alternatives that can significantly simplify the task of working with auto layout.

Storyboards

The structure of an iOS user interface is commonly represented by XIB files or storyboards created using Xcode’s Interface Builder utility. This tool allows developers to lay out an application’s user interface visually using a drag and drop metaphor.

For example, the following is a storyboard representing a simple view. The view contains two subviews whose positions will be automatically determined at runtime based on layout constraints. The constraints pin the subviews to the edges of the parent view as well as to each other, with a 16-pixel gap in between:

Running the application in portrait mode on an iPhone 8 produces the following results:

In landscape, the application looks like this:

While storyboards are undoubtedly the most common way to create auto layout constraints in iOS, they are not necessarily the most efficient. Using Interface Builder to visually establish every relationship can be awkward, especially when working with large or complex view hierarchies. Further, storyboards are not stored in a human-readable text format, which makes it difficult to identify changes across revisions.

Programmatically Defined Constraints

In addition to storyboards, constraints can also be managed programmatically. For example, the following Swift code produces the same results as the previous example:

// Red view
let redView = UIView()

redView.translatesAutoresizingMaskIntoConstraints = false
redView.backgroundColor = UIColor(red: 202.0 / 255.0, green: 53.0 / 255.0, blue: 56.0 / 255.0, alpha: 1.0)

view.addSubview(redView)

// Blue view
let blueView = UIView()

blueView.translatesAutoresizingMaskIntoConstraints = false
blueView.backgroundColor = UIColor(red: 59.0 / 255.0, green: 85.0 / 255.0, blue: 162.0 / 255.0, alpha: 1.0)

view.addSubview(blueView)

// Constraints
NSLayoutConstraint.activate([
    NSLayoutConstraint(item: redView, attribute: .top, relatedBy: .equal,
        toItem: topLayoutGuide, attribute: .bottom,
        multiplier: 1.0, constant: 0.0),
    NSLayoutConstraint(item: redView, attribute: .bottom, relatedBy: .equal,
        toItem: bottomLayoutGuide, attribute: .top,
        multiplier: 1.0, constant: 0.0),
    NSLayoutConstraint(item: redView, attribute: .leading, relatedBy: .equal,
        toItem: view, attribute: .leadingMargin,
        multiplier: 1.0, constant: 0.0),

    NSLayoutConstraint(item: blueView, attribute: .top, relatedBy: .equal,
        toItem: topLayoutGuide, attribute: .bottom,
        multiplier: 1.0, constant: 0.0),
    NSLayoutConstraint(item: blueView, attribute: .bottom, relatedBy: .equal,
        toItem: bottomLayoutGuide, attribute: .top,
        multiplier: 1.0, constant: 0.0),
    NSLayoutConstraint(item: blueView, attribute: .leading, relatedBy: .equal,
        toItem: redView, attribute: .trailing,
        multiplier: 1.0, constant: 16.0),
    NSLayoutConstraint(item: blueView, attribute: .trailing, relatedBy: .equal,
        toItem: view, attribute: .trailingMargin,
        multiplier: 1.0, constant: 0.0),

    NSLayoutConstraint(item: redView, attribute: .width, relatedBy: .equal,
        toItem: blueView, attribute: .width,
        multiplier: 1.0, constant: 0.0),
])

Because the constraints are established in code, it is easy to identify changes between revisions. Unfortunately, managing constraints programmatically is not particularly convenient. This simple layout required the definition of eight individual constraints. More complex layouts could quickly become untenable.

Stack Views

The UIStackView class, introduced in iOS 9, provides an alternative to managing constraints directly. Stack views automatically arrange their subviews in a vertical or horizontal line, and can be nested to create sophisticated layouts.

For example, the following code uses a stack view to produce results identical to the first two examples:

let view = UIStackView()

view.isLayoutMarginsRelativeArrangement = true
view.spacing = 16

// Red view
let redView = UIView()

redView.backgroundColor = UIColor(red: 202.0 / 255.0, green: 53.0 / 255.0, blue: 56.0 / 255.0, alpha: 1.0)

view.addArrangedSubview(redView)

// Blue view
let blueView = UIView()

blueView.backgroundColor = UIColor(red: 59.0 / 255.0, green: 85.0 / 255.0, blue: 162.0 / 255.0, alpha: 1.0)

view.addArrangedSubview(blueView)

// Width constraint
NSLayoutConstraint.activate([
    NSLayoutConstraint(item: redView, attribute: .width, relatedBy: .equal,
        toItem: blueView, attribute: .width,
        multiplier: 1.0, constant: 0.0),
])

While it is arguably more readable than the previous version, this example is still somewhat verbose. It also still requires the explicit creation of a layout constraint to manage the width relationship, which is not ideal.

Introducing MarkupKit

MarkupKit is an open-source framework for simplifying development of native iOS and tvOS applications. Among other things, it provides a collection of UIView subclasses whose sole responsibility is managing the size and position of their respective subviews:

  • LMRowView – arranges subviews in a horizontal line
  • LMColumnView – arranges subviews in a vertical line
  • LMAnchorView – optionally anchors subviews to one or more edges

These classes use layout constraints internally, allowing developers to easily take advantage of auto layout while eliminating the need to manage constraints directly.

For example, the following code uses an instance of LMRowView to replicate the results produced by the previous examples. The weight property is used to ensure that the views are the same width. This property, which is added to UIView by MarkupKit, specifies the amount of excess space the view would like to be given within its parent view, relative to all other weights. Since both views are assigned a weight of 1, they will each be given 1 / (1 + 1), or one-half, of the available space:

let view = LMRowView()

view.spacing = 16

// Red view
let redView = UIView()

redView.backgroundColor = LMViewBuilder.colorValue("#CA3538")
redView.weight = 1.0

view.addArrangedSubview(redView)

// Blue view
let blueView = UIView()

blueView.backgroundColor = LMViewBuilder.colorValue("#3B55A2")
blueView.weight = 1.0

view.addArrangedSubview(blueView)

Like stack views, layout views are easy to work with programmatically. However, unlike stack views, no manual constraint is manipulation required; MarkupKit uses the weight values to automatically establish the width relationship.

Additionally, the colorValue(_:) method of MarkupKit’s LMViewBuilder class is used to simplify color assignment. The logic is still a bit verbose though, and could become difficult to manage as view complexity increases.

Markup

MarkupKit’s namesake feature is its support for declarative view construction. Using markup, the view hiearchy created in the previous example can be represented entirely as follows:

<LMRowView spacing="16">
    <UIView backgroundColor="#ca3538" weight="1"/>
    <UIView backgroundColor="#3b55a2" weight="1"/>
</LMRowView>

The markup is loaded using the view(withName:owner:root:) method of LMViewBuilder. This method is similar to the loadNibNamed(_:owner:options:) method of the NSBundle class, and returns the root element of the view hiearchy declared in the document:

view = LMViewBuilder.view(withName: "ViewController", owner: self, root: nil)

Unlike all of the preceding examples, this version is extremely concise. It is also much more readable: the element hierarchy declared in the document parallels the resulting view hiearchy, making it easy to understand the relationships between views.

For example, the periodic table shown below was constructed using a combination of MarkupKit’s layout views and UILabel instances:

Creating this view in Interface Builder would be an arduous task. Creating it programmatically would be even more difficult. However, in markup it is almost trivial. The complete source code for this example can be found here.

Summary

This article provided an overview of how layout constraints are typically managed in an iOS application and introduced MarkupKit, an open-source framework that can significantly simplify the task of working with auto layout in iOS.

For more information and additional examples, see the MarkupKit README.

MarkupKit 4.2 Released

MarkupKit 4.2 is now available for download. This release adds support for formatting the result of binding expressions.

In MarkupKit, attributes whose values begin with “$” represent data bindings. The text following the “$” character represents an expression to which the corresponding view property will be bound. Any time the value of the bound expression changes, the target property in the view will be automatically updated. MarkupKit monitors property changes using key-value observing, and evaluates expressions using the Foundation framework’s NSExpression class.

For example, a view controller might define a bindable property called name as follows:

class ViewController: UIViewController {
    @objc dynamic var name: String?

    ...
}

The following markup establishes a binding between the text property of a UILabel instance and the controller’s name property. Any changes to name will be automatically reflected in the label:

<UILabel text="$name"/>

Binding Expressions

Binding expressions are not limited to simple properties, however. For example, a custom table view cell might use an instance of the following class as a model:

class Row: NSObject, Decodable {
    @objc var heading: String?
    @objc var detail: String?
}

class CustomCell: LMTableViewCell {
    @objc dynamic var row: Row!

    ...
}

This markup binds the text property of the heading and detail views to the row’s heading and detail properties, respectively. Any time the value of row changes, the labels will be updated:

<LMColumnView>
    <UILabel text="$row.heading"/>
    <UILabel text="$row.detail"/>
</LMColumnView>

Expression Formatters

Binding expressions are also not limited to string values. For example, they can refer to numeric or date properties, and can even contain mathematical operations. It is not possible to bind the result of such expressions to string-based target properties directly. However, formatters can be used to convert a bound value to an appropriate textual representation.

Formatters are applied by appending a format specifier to a binding declaration. A format specifier contains the name of the formatter to apply, along with any associated arguments to the formatter. The format specification is separated from the actual expression by a double-colon (“::”).

For example, the following markup uses a date formatter to convert a person’s date of birth (represented by an NSDate instance) to a short-format date string:

<UILabel text="$person.dateOfBirth::date;dateStyle=short;timeStyle=none"/>

This markup converts a double value to a string using a maximum of three decimal places:

<UILabel text="$averageAge::number;numberStyle=decimal;maximumFractionDigits=3"/>

Formatters are obtained via the following method, which MarkupKit adds to UIResponder:

- (NSFormatter *)formatterWithName:(NSString *)name arguments:(NSDictionary *)arguments;

This method is called on the document’s owner any time the value of a bound, formatted expression changes. The default implementation provides support for “date” and “number” formatters, which correspond to the NSDateFormatter and NSNumberFormatter Foundation classes, respectively. The arguments represent the properties that will be set on the formatter to configure its behavior. Owning classes can override this method to support custom formatters.

Summary

Binding expressions in MarkupKit allow developers to declaratively establish relationships between a model object and a target view, enabling a simple and intuitive reactive development model. For more information, see the project README.

Project Marzipan?

Rumors have been swirling recently about “Project Marzipan”, an as-yet unreleased Apple project some originally thought might allow iOS apps to run on macOS. I’ve been personally hoping for something along the lines of UXKit, a private framework Apple reportedly used to develop the current version of Photos for the Mac.

However, based on a recent post by Daring Fireball’s John Gruber, it seems as though “Project Marzipan” (which is apparently now being called something else) may actually be a “declarative control API”:

The general idea is that rather than writing classic procedural code to, say, make a button, then configure the button, then position the button inside a view, you instead declare the button and its attributes using some other form. HTML is probably the most easily understood example. In HTML you don’t procedurally create elements like paragraphs, images, and tables — you declare them with tags and attributes in markup.

This sounds a lot like MarkupKit. It will be very interesting to see how this turns out when (or if) it is ultimately announced, which could apparently happen as early as WWDC 2018 but more likely 2019.

MarkupKit 4.1 Released

MarkupKit 4.1 is now available for download. This release adds support for live preview of markup documents within Xcode:

View classes tagged with IB_DESIGNABLE or @IBDesignable can now call the new preview:owner: method MarkupKit adds to the UIView class to validate markup changes at design time, avoiding the need to launch the iOS simulator. If an error occurs while loading the document, a label containing the error message will be overlaid on top of the view instance, allowing typos and other errors to be quickly identified.

For example, the following class provides a preview of the DetailViewController.xml document in the MarkupKit sample application. The view’s implementation of the prepareForInterfaceBuilder() method provides placeholder content; a temporary controller instance is used to ensure that outlets, actions, and bindings are handled properly:

@IBDesignable
class DetailViewControllerPreview: LMRootView {
    override func prepareForInterfaceBuilder() {
        let owner = DetailViewController(nibName: nil, bundle: nil)

        preview("DetailViewController", owner: owner)

        owner.iconImageView.image = UIImage(named: "BeachIcon", in: Bundle(for: type(of: self)), compatibleWith: nil)

        owner.headingLabel.text = "Heading Text"
        owner.detailLabel.text = "Detail Message"
    }
}

Note that this class is only used at design time – the view controller is still responsible for loading the view document at run time:

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

Live preview can significantly reduce development time, since it eliminates the round trip through the simulator that is typically required to test an update. Using live preview, successive updates can be quickly verified, and the simulator launched only when the desired layout has been achieved.

For more information, see the project README.

Implementing Auto-Complete with UITextField

I recently wanted to add a Safari-like auto-complete feature to an iOS app I’ve been working on. Specifically, I wanted the app to proactively suggest a complete word based on some initial characters entered by the user, similar to how Safari suggests URLs based on the first few letters in a web address:

As in Safari, tapping Return would allow the user to confirm the suggestion.

Since this is not a feature that Apple provides “out of the box”, I thought I would share the approach I took in case it is of interest to anyone.

In this example, the text field will suggest values for the user’s favorite color:

As the user types, a list of options is consulted to determine which value to suggest. For example:

Suggestions are defined as an array of strings:

let suggestions = [
    "red",
    "orange",
    "yellow",
    "green",
    "blue",
    "purple"
]

To handle user input, the view controller assigns itself as the text field’s delegate and implements the textField(_:shouldChangeCharactersIn:replacementString:) method, as shown below:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    return !autoCompleteText(in: textField, using: string, suggestions: suggestions)
}

This method simply invokes the following method, which searches the suggestion list for the first entry with a prefix matching the user’s input. It then updates the text value with the identified suggestion and selects the remaining characters in the text field:

func autoCompleteText(in textField: UITextField, using string: String, suggestions: [String]) -> Bool {
    if !string.isEmpty,
        let selectedTextRange = textField.selectedTextRange, selectedTextRange.end == textField.endOfDocument,
        let prefixRange = textField.textRange(from: textField.beginningOfDocument, to: selectedTextRange.start),
        let text = textField.text(in: prefixRange) {
        let prefix = text + string
        let matches = suggestions.filter { $0.hasPrefix(prefix) }

        if (matches.count > 0) {
            textField.text = matches[0]

            if let start = textField.position(from: textField.beginningOfDocument, offset: prefix.characters.count) {
                textField.selectedTextRange = textField.textRange(from: start, to: textField.endOfDocument)

                return true
            }
        }
    }

    return false
}

The method returns true if a match was found and false otherwise. The delegate method returns the inverse of this value so the text field will continue to process keystrokes when a match is not found.

Finally, the controller implements the delegate’s textFieldShouldReturn(_:) method to “confirm” the suggestion:

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()

    return true
}

Note that the text field’s autocapitalizationType and autocorrectionType properties were set to .none and .no, respectively. Disabling auto-capitalization ensures that the lookup logic will correctly identify matches, since all of the suggestions begin with lowercase letters. Turning off auto-correction ensures that iOS’s built-in suggestion bar is not displayed, since suggestions will be made by the text field itself.

Hope you find it useful. 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

Creating a Universal Framework in Xcode 9

A while back, I posted a script for creating a universal iOS framework (i.e. one that will run in both the simulator as well as on an actual device) in Xcode 7. The following is an updated version for use with Xcode 9. This version should work with both Swift and Objective-C projects:

FRAMEWORK=<framework name>

BUILD=build
FRAMEWORK_PATH=$FRAMEWORK.framework

# iOS
rm -Rf $FRAMEWORK-iOS/$BUILD
rm -f $FRAMEWORK-iOS.framework.tar.gz

xcodebuild archive -project $FRAMEWORK-iOS/$FRAMEWORK-iOS.xcodeproj -scheme $FRAMEWORK -sdk iphoneos SYMROOT=$BUILD
xcodebuild build -project $FRAMEWORK-iOS/$FRAMEWORK-iOS.xcodeproj -target $FRAMEWORK -sdk iphonesimulator SYMROOT=$BUILD

cp -RL $FRAMEWORK-iOS/$BUILD/Release-iphoneos $FRAMEWORK-iOS/$BUILD/Release-universal
cp -RL $FRAMEWORK-iOS/$BUILD/Release-iphonesimulator/$FRAMEWORK_PATH/Modules/$FRAMEWORK.swiftmodule/* $FRAMEWORK-iOS/$BUILD/Release-universal/$FRAMEWORK_PATH/Modules/$FRAMEWORK.swiftmodule

lipo -create $FRAMEWORK-iOS/$BUILD/Release-iphoneos/$FRAMEWORK_PATH/$FRAMEWORK $FRAMEWORK-iOS/$BUILD/Release-iphonesimulator/$FRAMEWORK_PATH/$FRAMEWORK -output $FRAMEWORK-iOS/$BUILD/Release-universal/$FRAMEWORK_PATH/$FRAMEWORK

tar -czv -C $FRAMEWORK-iOS/$BUILD/Release-universal -f $FRAMEWORK-iOS.tar.gz $FRAMEWORK_PATH $FRAMEWORK_PATH.dSYM

When located in the same directory as the .xcodeproj file, this script will invoke xcodebuild twice on a framework project and join the resulting binaries together into a single universal binary. It will then package the framework up in a gzipped tarball and place it in the same directory.

However, apps that contain “fat” binaries like this don't pass app store validation. Before submitting an app containing a universal framework, the binaries need to be trimmed so that they include only iOS-native code. The following script can be used to do this:

FRAMEWORK=$1
echo "Trimming $FRAMEWORK..."

FRAMEWORK_EXECUTABLE_PATH="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/$FRAMEWORK.framework/$FRAMEWORK"

EXTRACTED_ARCHS=()

for ARCH in $ARCHS
do
    echo "Extracting $ARCH..."
    lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
    EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
done

echo "Merging binaries..."
lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
rm "${EXTRACTED_ARCHS[@]}"

rm "$FRAMEWORK_EXECUTABLE_PATH"
mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"

echo "Done."

To use this script:

  1. Place the script in your project root directory and name it trim.sh or something similar
  2. Create a new “Run Script” build phase after the “Embed Frameworks” phase
  3. Rename the new build phase to “Trim Framework Executables” or similar (optional)
  4. Invoke the script for each framework you want to trim (e.g. ${SRCROOT}/trim.sh)

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

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