Getting Started with 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.

This tutorial introduces the MarkupKit framework by demonstrating how to construct a simple "Hello, World"-style application using markup:

Tapping the button causes a greeting to appear:

The code examples are written in Swift. Xcode 9 and iOS 9 or later are required.

Get MarkupKit

MarkupKit is developed as a freely available open-source project on GitHub. The latest release can be downloaded here.

The file named MarkupKit-iOS.tar.gz contains the iOS framework bundle, MarkupKit.framework. Download this archive and expand it somewhere on your local system.

Create the Xcode Project

In Xcode, select File | New | Project to create a new iOS project. Select the iOS/Application/Single View App template and click the "Next" button. Name the product "HelloMarkupKit" and select Swift as the development language.

Next, add the MarkupKit framework to the project:

  • Select the project root node in the Project Navigator view
  • Select the "HelloMarkupKit" target
  • Select the "General" tab
  • In the Finder, navigate to the location where you unzipped MarkupKit.framework
  • Drag MarkupKit.framework from the Project Navigator to the "Embedded Binaries" section
  • In the dialog that appears, ensure that "Copy items if needed" is checked and click "Finish"

Create the View Controller

In Project Navigator, select ViewController.swift. Import the MarkupKit module by adding the following line to the import section:

import MarkupKit

Within the ViewController class, add an outlet for the label that will be used to display the greeting:

@IBOutlet var greetingLabel: UILabel!

Next, add the following method, which will load the view from markup. The markup document, ViewController.xml, will be created later:

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

Finally, add an action to handle taps on the "Say Hello" button:

@IBAction func sayHello() {
    greetingLabel.text = "Hello, World!";
}

Your final view controller should look something like this:

import UIKit
import MarkupKit
    
class ViewController: UIViewController {
    @IBOutlet var greetingLabel: UILabel!
    
    override func loadView() {
        view = LMViewBuilder.view(withName: "ViewController", owner: self, root: nil)
    }
    
    @IBAction func sayHello() {
        greetingLabel.text = "Hello, World!";
    }
}

Create the View

Select File | New | File… from the menu. In the dialog that appears, select iOS > Other > Empty and click "Next". Name the file ViewController.xml, ensure that the "HelloMarkupKit" project target is selected, and click the "Create" button. Add the following markup to the newly created document:

<?xml version="1.0" encoding="UTF-8"?>

<LMColumnView backgroundColor="white">
    <LMSpacer height="96"/>

    <UIButton style="systemButton" title="Say Hello" onPrimaryActionTriggered="sayHello"/>
    <UILabel id="greetingLabel" font="headline" textAlignment="center"/>

    <LMSpacer/>
</LMColumnView>

This markup declares a simple view hierarchy containing a label and a button. View properties such as font and textAlignment are set using XML attributes. The "id" attribute creates an outlet for the label instance, which is used to associate the label with the instance variable defined in the controller class. The "onPrimaryActionTriggered" attribute defines an action handler for the button and associates it with the sayHello() method defined by the controller.

An instance of LMColumnView is used to automatically arrange the view elements in a vertical line. LMColumnView is an example of a layout view, a view whose sole responsibility is managing the size and position of its subviews. LMSpacer is used to provide fixed and flexible spacing at the top and bottom of the view, respectively.

Build and Run the App

The application is now ready to run. Ensure that "HelloMarkupKit" is selected in the scheme drop-down and launch the application by selecting Product | Run from the menu or by clicking the "Run" button:

Tapping the "Say Hello" button displays the greeting "Hello, World!":

Next Steps

This document introduced the MarkupKit framework by demonstrating how to build a simple, "Hello, World"-style application using markup. 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