PL

Let's Build with PLRelational, Part 2

September 28, 2017, by Chris Campbell

This article picks up where Part 1 left off, continuing our look at building a real application using PLRelational.

For more background on PLRelational, check out these other articles in the series:

In Part 1 of this article, we used PLRelational to build a portion of a to-do application for macOS. In this article, we will follow the same process and focus on building out the right-hand side of the application, specifically the detail view.

The detail view allows the user to change the status or text of the selected to-do item, apply tags, add notes, or delete the item. Click the "Play" button below to see the detail view in action:

As in Part 1, we've broken the right half of the application down into distinct functional requirements that describe how each piece of the UI should behave.

The following interactive screenshot shows the right half of the app in its completed state. Each red dot corresponds to a single functional requirement. Hover over a dot to read a summary of the requirement, then click to see how we implemented it using PLRelational.

6 7 8 9 10 11 12 13

Let's explore how we implemented this half of the application. (To follow along with the complete source code in Xcode, clone the PLRelational repository and open Example Apps > TodoApp in the project.)

REQ-6: Selected Item Detail

A detail view on the right side that allows the user to change information about the to-do item that is currently selected in the list. If there is no selection, the detail view should be hidden and replaced by a gray "No Selection" label.

There are two parts to this requirement.

First we need to bind the selection state of the list view to our selectedItemIDs relation. Every time the user selects an item in the list, the identifier of that item will be poked into the selectedItemIDs relation. In later steps, we will join that relation with others to derive data related to the selected item, such as its title.

Once we have the selection binding in place, we can expose a hasSelection property that resolves to true whenever there is an item selected in the list view. This makes it easy to set up bindings at the view level to toggle between showing the "No Selection" label and the detail view:

class Model { ...
    /// Resolves to the item that is selected in the list of to-do items.
    lazy var selectedItems: Relation = {
        return self.selectedItemIDs.join(self.items)
    }()

    /// Resolves to `true` when an item is selected in the list of to-do items.
    lazy var hasSelection: AsyncReadableProperty<Bool> = {
        return self.selectedItems.nonEmpty.property()
    }()
}

class ChecklistViewModel { ...
    /// Holds the ID of the to-do item that is selected in the list view.  This
    /// is a read/write property that is backed by UndoableDatabase, meaning that
    /// even selection changes can be undone (which admittedly is taking things
    /// to an extreme but we'll keep it like this for demonstration purposes).
    lazy var itemsListSelection: AsyncReadWriteProperty<Set<RelationValue>> = {
        return self.model.selectedItemIDs
            .undoableAllRelationValues(self.model.undoableDB, "Change Selection")
    }()
}

class ChecklistView { ...
    init() { ...
        // Bidirectionally bind list view selection to the view model
        listView.selection <~> model.itemsListSelection
    }
}

class AppDelegate { ...
    func applicationDidFinishLaunching() { ...
        // Toggle the "No Selection" label and detail view depending
        // on the selection state
        detailView.visible <~ model.hasSelection
        noSelectionLabel.visible <~ not(model.hasSelection)
    }
}

Note that we define itemsListSelection as an undoable property, just to show that even things like selection state can be modeled using relations and can participate in the undo system. (Lightroom is an example of an application where selection changes are part of the history stack.)

REQ-7: Selected Item Completed Checkbox

A checkbox at the top-left of the detail view. This should reflect whether the selected to-do item is pending (unchecked) or completed (checked). The behavior of this checkbox is the same as described in REQ-3, except that it controls the selected item's position in the list.

Hooking up the checkbox in the detail view is almost identical to the way we did it for list cells in REQ-3. In fact, we use the same itemCompleted function from Model, except this time we pass it a Relation that focuses on the status value of the selected item:

class DetailViewModel { ...
    /// The completed status of the item.
    lazy var itemCompleted: AsyncReadWriteProperty<CheckState> = {
        let relation = self.model.selectedItems.project(Item.status)
        return self.model.itemCompleted(relation, initialValue: nil)
    }()
}

class DetailView { ...
    init() { ...
        // Bidirectionally bind checkbox state to the view model
        checkbox.checkState <~> model.itemCompleted
    }
}

REQ-8: Selected Item Title Field

A text field to the right of the checkbox in the detail view. This should reflect the title of the selected to-do item. If the user changes the title in the detail view and presses enter, the title should also be updated in the selected list cell.

Just like with the last step, wiring up the text field in the detail view follows the same approach used in REQ-4 (list cell text field):

class DetailViewModel { ...
    /// The item's title.  This is a read/write property that is backed
    /// by UndoableDatabase, so any changes made to it in the text field
    /// can be rolled back by the user.
    lazy var itemTitle: AsyncReadWriteProperty<String> = {
        let titleRelation = self.model.selectedItems.project(Item.title)
        return self.model.itemTitle(titleRelation, initialValue: nil)
    }()
}

class DetailView { ...
    init() { ...
        // Bidirectionally bind title field to the view model
        titleField.string <~> model.itemTitle
    }
}

REQ-9: Assign Tag Combo Box

A combo box with placeholder "Assign a tag". The pull-down menu should include a list of available tags (all tags that haven't yet been applied; if a tag has been applied it should not appear in the menu). The user can also type in an existing tag or new tag. If the user clicks or enters a tag, that tag should be added to the set of applied tags for the selected to-do item.

Combo boxes are complex controls (they are part menu and part text field), so let's break this up into a few parts. First we'll show how we populate the menu of available tags:

class Model { ...
    /// Resolves to the set of tags that are associated with the selected to-do
    /// item (assumes there is either zero or one selected items).
    lazy var tagsForSelectedItem: Relation = {
        return self.selectedItemIDs
            .join(self.itemTags)
            .join(self.tags)
            .project([Tag.id, Tag.name])
    }()

    /// Resolves to the set of tags that are not yet associated with the
    /// selected to-do item, i.e., the available tags.
    lazy var availableTagsForSelectedItem: Relation = {
        // This is simply "all tags" minus "already applied tags", nice!
        return self.tags
            .difference(self.tagsForSelectedItem)
    }()
}

class DetailViewModel { ...
    /// The tags that are available (i.e., not already applied) for the selected
    /// to-do item, sorted by name.  We use `fullArray` so that the entire array
    /// is delivered any time there is a change; this helps to make it
    /// compatible with the `EphemeralComboBox` class.
    lazy var availableTags: AsyncReadableProperty<[RowArrayElement]> = {
        return self.model.availableTagsForSelectedItem
            .arrayProperty(idAttr: Tag.id, orderAttr: Tag.name)
            .fullArray()
    }()
}

class DetailView { ...
    init() { ...
        // Bind combo box menu items to the view model
        tagComboBox.items <~ model.availableTags
    }
}

Next we'll show how to handle when an existing tag is selected in the menu:

class Model { ...
    /// Applies an existing tag to the given to-do item.
    func addExistingTag(_ tagID: TagID, to itemID: ItemID) {
        undoableDB.performUndoableAction("Add Tag", {
            self.itemTags.asyncAdd([
                ItemTag.itemID: itemID,
                ItemTag.tagID: tagID
            ])
        })
    }
}

class DetailViewModel { ...
    /// Adds an existing tag to the selected to-do item.
    lazy var addExistingTagToSelectedItem: ActionProperty<RelationValue> = ActionProperty { tagID in
        self.model.addExistingTag(TagID(tagID), to: self.itemID.value!!)
    }
}

class DetailView { ...
    init() { ...
        // When a tag is selected, add that tag to the selected to-do item
        tagComboBox.selectedItemID ~~> model.addExistingTagToSelectedItem
    }
}

Finally we'll show how to handle when a new tag is entered in the combo field:

class Model { ...
    /// Creates a new tag and applies it to the given to-do item.
    func addNewTag(named name: String, to itemID: ItemID) {
        let tagID = TagID()

        undoableDB.performUndoableAction("Add New Tag", {
            self.tags.asyncAdd([
                Tag.id: tagID,
                Tag.name: name
            ])

            self.itemTags.asyncAdd([
                ItemTag.itemID: itemID,
                ItemTag.tagID: tagID
            ])
        })
    }
}

class DetailViewModel { ...
    /// Creates a new tag of the given name and adds it to the selected to-do item.
    lazy var addNewTagToSelectedItem: ActionProperty<String> = ActionProperty { name in
        // See if a tag already exists with the given name
        let itemID = self.itemID.value!!
        let existingIndex = self.model.allTags.value?.index(where: {
            let rowName: String = $0.data[Tag.name].get()!
            return name == rowName
        })
        if let index = existingIndex {
            // A tag already exists with the given name, so apply that tag
            // rather than creating a new one
            let elem = self.model.allTags.value![index]
            let tagID = TagID(elem.data)
            self.model.addExistingTag(tagID, to: itemID)
        } else {
            // No tag exists with that name, so create a new tag and apply
            // it to this item
            self.model.addNewTag(named: name, to: itemID)
        }
    }
}

class DetailView { ...
    init() { ...
        // Add a new tag each time a string is entered into the combo field
        tagComboBox.committedString ~~> model.addNewTagToSelectedItem
    }
}

REQ-10: Selected Item Tags List

A list of tags that have been applied to the selected to-do item.

Setting up this list view follows roughly the same process that we used for the list of to-do items in REQ-2 through REQ-5:

  1. Define the ListViewModel. In this case, the list will contain the array of tags for the selected to-do item, sorted by name.
  2. Define a property to hold the selection state of the list.
  3. Define properties for each element of the list cell. In this case, there is a single text field for the tag name, and we want to allow the user to edit the name so we set it up as a bidirectional binding.
  4. Bind the views to those properties we defined in the View Model layer.

Here's what that process looks like in code form:

class Model { ...
    /// Returns a property that reflects the tag name.
    func tagName(for tagID: TagID, initialValue: String?) -> AsyncReadWriteProperty<String> {
        return self.tags
            .select(Tag.id *== tagID)
            .project(Tag.name)
            .undoableOneString(undoableDB, "Change Tag Name", initialValue: initialValue)
    }
}

class DetailViewModel { ...
    /// The tags associated with the selected to-do item, sorted by name.
    private lazy var itemTags: ArrayProperty<RowArrayElement> = {
        return self.model.tagsForSelectedItem
            .arrayProperty(idAttr: Tag.id, orderAttr: Tag.name)
    }()

    /// The model for the tags list, i.e., the tags that have been applied to the
    /// selected to-do item.
    lazy var tagsListViewModel: ListViewModel<RowArrayElement> = {
        return ListViewModel(
            data: self.itemTags,
            cellIdentifier: { _ in "Cell" }
        )
    }()

    /// Returns a read/write property that resolves to the name for the given tag.
    func tagName(for row: Row) -> AsyncReadWriteProperty<String> {
        let tagID = TagID(row)
        let name: String? = row[Tag.name].get()
        return self.model.tagName(for: tagID, initialValue: name)
    }

    /// Holds the ID of the tag that is selected in the tags list view.
    lazy var selectedTagID: ReadWriteProperty<Set<RelationValue>> = {
        return mutableValueProperty(Set())
    }()
}

class DetailView { ...
    init() { ...
        // Bind outline view to the tags list view model
        tagsListView = ListView(model: model.tagsListViewModel,
                                outlineView: tagsOutlineView)

        // Bidirectionally bind list view selection to the view model
        tagsListView.selection <~> model.selectedTagID

        tagsListView.configureCell = { view, row in
            // Bidirectionally bind tag name field to the view model
            let textField = view.textField as! TextField
            textField.string.unbindAll()
            textField.string <~> model.tagName(for: row)
        }
    }
}

REQ-11: Selected Item Notes

A text view that allows the user to type in notes about the selected to-do item.

This step is straightforward. There is a single read/write property that reflects the notes value for the selected to-do item, and we bidirectionally bind it to the TextView:

class Model { ...
    /// Returns a property that reflects the selected item's notes.
    lazy var selectedItemNotes: AsyncReadWriteProperty<String> = {
        return self.selectedItems
            .project(Item.notes)
            .undoableOneString(self.undoableDB, "Change Notes")
    }()
}

class DetailViewModel { ...
    /// The item's notes.
    lazy var itemNotes: AsyncReadWriteProperty<String> = {
        return self.model.selectedItemNotes
    }()
}

class DetailView { ...
    init() { ...
        // Bidirectionally bind content of notes text view to the view model
        notesTextView.text <~> model.itemNotes
    }
}

The TextView class will take care of delivering changes via its text property whenever the user is done editing (as a result of a change in first responder, for example).

REQ-12: Selected Item "Created On" Label

A read-only label that shows when the selected to-do item was created, e.g. "Created on Sep 1, 2017".

This step is also relatively simple. It's a read-only label, so we only have to worry about transforming the raw created timestamp string, as stored in the items relation, to a display-friendly string:

class DetailViewModel { ...
    /// The text that appears in the "Created on <date>" label.  This
    /// demonstrates the use of `map` to convert the raw timestamp string
    /// (as stored in the relation) to a display-friendly string.
    lazy var createdOn: AsyncReadableProperty<String> = {
        return self.model.selectedItems
            .project(Item.created)
            .oneString()
            .map{ "Created on \(displayString(from: $0))" }
            .property()
    }()
}

class DetailView { ...
    init() { ...
        // Bind "Created on <date>" label to the view model
        createdOnLabel.string <~ model.createdOn
    }
}

Let's dissect that createdOn declaration:

// Start with the `selectedItems` relation
self.model.selectedItems

// Take (`project`) the `created` attribute of the `selectedItems` relation
.project(Item.created)

// Derive a `Signal` that carries the raw timestamp string when there is a
// selected to-do item (or an empty string when there is no selection)
.oneString()

// Convert the raw timestamp string into a display-friendly one
.map{ "Created on \(displayString(from: $0))" }

// Lift the `Signal` into an `AsyncReadableProperty` that offers the latest value
.property()

This is another example of the Relation -> Signal -> Property pattern that we discussed earlier in REQ-5. You will encounter this pattern frequently when working with PLRelationalBinding, so it's a good idea to familiarize yourself with it.

REQ-13: Delete Selected Item Button

A delete button. If the user clicks this button, the selected item should be deleted (removed from the list entirely) and the list selection should be cleared (no item selected).

We're almost done! Deletion is an interesting case because we need to be able to delete not just the row in the items relation but also the associated rows in itemTags and selectedItemIDs. If we had the ID of the selected item handy we could simply issue three asyncDelete calls and be done with it, but where's the fun in that? Nowhere, that's where. So instead, let's demonstrate the fancy cascadingDelete function:

class Model { ...
    /// Deletes the row associated with the selected item and
    /// clears the selection.  This demonstrates the use of
    /// `cascadingDelete`, which is kind of overkill for this
    /// particular case but does show how easy it can be to
    /// clean up related data with a single call.
    func deleteSelectedItem() {
        undoableDB.performUndoableAction("Delete Item", {
            // We initiate the cascading delete by first removing
            // all rows from `selectedItemIDs`
            self.selectedItemIDs.cascadingDelete(
                true, // `true` here means "all rows"
                affectedRelations: [
                    self.items, self.selectedItemIDs, self.itemTags
                ],
                cascade: { (relation, row) in
                    if relation === self.selectedItemIDs {
                        // This row was deleted from `selectedItemIDs`;
                        // delete corresponding rows from `items`
                        // and `itemTags`
                        let itemID = ItemID(row)
                        return [
                            (self.items, Item.id *== itemID),
                            (self.itemTags, ItemTag.itemID *== itemID)
                        ]
                    } else {
                        // Nothing else to clean up
                        return []
                    }
                }
            )
        })
    }
}

class DetailViewModel { ...
    /// Deletes the selected item.  This demonstrates the use of
    /// `ActionProperty` to expose an imperative (side effect producing)
    /// action as a property that can easily be bound to a `Button`.
    lazy var deleteItem: ActionProperty<()> = ActionProperty { _ in
        self.model.deleteSelectedItem()
    }
}

class DetailView { ...
    init() { ...
        // When the button is clicked, delete the selected item
        deleteButton.clicks ~~> model.deleteItem
    }
}

This was a pretty straightforward use of cascadingDelete, but it offers other features (not used in this particular example) that really come in handy when deleting lots of interconnected data and repairing connections affected by the deletion. Additionally, there is a companion to cascadingDelete called treeDelete that simplifies deletion of hierarchical data. For more on these, check out MutableRelationCascadingDelete.swift in the repository along with some example uses in RelationTests.swift.

Quiet Victories

While most of this article was focused on showing how succinctly and easily we can build a user interface using PLRelational, it's worth noting the things you didn't see. These are just a few of the things you no longer have to worry about, because PLRelational does the hard work for you:

  • Objects, object graphs, observers: In a traditional application, we would have defined classes or structs for things like Item and Tag, and we would have had to maintain some sort of manager/controller to handle loading them from disk (and saving them back again), maintaining the relationships in a complex graph, updating those objects and sending out notifications to observers when something has changed, and so forth. With PLRelational, all of this gets handled through Relations and their reactive extensions; in our to-do application, we never explicitly loaded data into objects, and data flows from disk up to the user interface and back again through those Relations.

  • Undo/Redo: In a traditional application, we would have had to register with the UndoManager separate imperative methods for each undoable action: one to apply the change, and one to roll it back (along with the required support in the user interface layer to correctly react to those changes). With PLRelational, deltas can be automatically computed with each change, so implementing undo/redo support in your application is as easy as providing a "forward" transaction. When the user performs an undo or redo operation, the framework simply reverts the relations to a particular snapshot, and the user interface layer automatically updates by virtue of the reactive bindings.

  • Table updates: In a traditional Cocoa application, we would have had to implement the NSTableViewDataSource and NSTableViewDelegate protocols, writing fragile logic to coordinate updates to the table view in response to some changes in the object graph. With PLRelational, the ArrayProperty and ListView classes work in concert, tracking changes made in the relations and delivering them in a nice bundle to the underlying NSTableView with almost no custom logic needed on your part.

Wrapping Up

Over the course of this two-part article, we demonstrated how to use PLRelational to model source relations and then use a reactive-relational style of programming to build a working macOS application.

The vocabulary offered by PLRelational makes it easy to declare — and reason about — the relationships between data in your application and how it is presented in the user interface. Although we focused on building a macOS application in this article, the same techniques can be used to simplify development of iOS applications as well. (And perhaps we will demonstrate just that in a future article!)

The complete source code for this application is available in the PLRelational repository under the Examples/TodoApp directory and can be run directly from the Xcode project.

Finally, if you've been experimenting with PLRelational and have thoughts to share, or have a topic related to these frameworks that you'd like us to cover, please tweet at us or mail it in!

Let's Build with PLRelational, Part 1

September 18, 2017, by Chris Campbell

This is our latest entry in a series of articles on PLRelational. For more background, check out these other articles in the series:

Since we started opening up about PLRelational, a number of developers have asked us how it compares to some particular existing technology, e.g. Core Data, Rx, Cocoa Bindings, or SQLite. It's a good question, but before we can clearly explain the differences, we should first dissect a working application that was built using PLRelational.

Our goal with this article is to give a big picture look at PLRelational and how it can be used to build an actual application. We will show how you can do things the PLRelational Way, and that will give us a baseline to help compare and contrast to existing technologies in a future article.

We believe PLRelational[+Binding] gives you a unique, expressive vocabulary for declaring how your UI is connected. Once you've declared those relationships, the frameworks can do a lot of heavy lifting, simplifying the way you build an application. As we will demonstrate here, there are a few patterns commonly used when working with PLRelational that can help bring a sense of order and sanity to application development.

What Should We Build?

To get started, we need to think of a relatively constrained type of application to build, and there's no better (or more clichéd) example than a to-do app, so let's run with that. In a nutshell, our application will allow the user to:

  • enter new to-do items
  • mark an item as completed
  • change the item text
  • add tags to an item
  • add notes for an item
  • delete an item

When the application is finished, it should look something like the following, except a bit prettier:

Additionally, our application should automatically save everything to disk, and it should allow the user to undo or redo any change.

How Do We Organize It?

When building applications using PLRelational, we use an approach that is very similar to MVVM (Model / View / View Model).

In traditional MVVM-based apps, it is often the case that the Model layer is based on some sort of in-memory object graph that is pulled from (and stored back to) some source, e.g. files on disk, a database, or the network.

With PLRelational, we forego the traditional object graph and instead build the Model by declaring how our source relations are laid out (most likely backed by some on-disk storage, e.g. plists or SQLite) and defining some base operations that modify those relations.

As seen in this diagram, when building with PLRelational the implementation of each piece of the user interface is typically spread across three layers:

  • In the Model layer, we declare our source Relations and define transactions that modify those Relations.

  • In the View Model layer, we declare Relation / Signal / Property chains that express how the data from our Model layer is to be transformed for display. If the interaction is two-way, as is the case for something like a text field, we'll also declare how changes made by the user should be transformed before being stored back via the Model layer.

  • In the View layer, we define the views and bind them to the Property chains that we expressed in our View Model layer.

Note that in a larger application, you would probably consider splitting up the Model layer over multiple classes for improved separation of concerns and modularity. Our to-do application on the other hand is relatively simple, so a single Model class will be sufficient to encapsulate all the relations we care about.

Declaring Relations

Let's begin building our Model layer by declaring the attributes and layout of our 4 relations, which will be stored on disk using property list files:

/// Scheme for the `item` relation that holds the to-do items.
enum Item {
    static let id = Attribute("item_id")
    static let title = Attribute("title")
    static let created = Attribute("created")
    static let status = Attribute("status")
    static let notes = Attribute("notes")
    fileprivate static var spec: Spec { return .file(
        name: "item",
        path: "items.plist",
        scheme: [id, title, created, status, notes],
        primaryKeys: [id]
    )}
}

/// Scheme for the `tag` relation that holds the named tags.
enum Tag {
    static let id = Attribute("tag_id")
    static let name = Attribute("name")
    fileprivate static var spec: Spec { return .file(
        name: "tag",
        path: "tags.plist",
        scheme: [id, name],
        primaryKeys: [id]
    )}
}

/// Scheme for the `item_tag` relation that associates zero
/// or more tags with a to-do item.
enum ItemTag {
    static let itemID = Item.id
    static let tagID = Tag.id
    fileprivate static var spec: Spec { return .file(
        name: "item_tag",
        path: "item_tags.plist",
        scheme: [itemID, tagID],
        primaryKeys: [itemID, tagID]
    )}
}

/// Scheme for the `selected_item` relation that maintains
/// the selection state for the list of to-do items.
enum SelectedItem {
    static let id = Item.id
    fileprivate static var spec: Spec { return .transient(
        name: "selected_item",
        scheme: [id],
        primaryKeys: [id]
    )}
}

We use enums to provide a namespace for the Attributes associated with each Relation. For each one we also declare a Spec (typealias for PlistDatabase.RelationSpec) that tells our PlistDatabase how the relations will be stored on disk in property list format. In the case of ItemTag and SelectedItem, we use identifier attributes that act as foreign keys referring to the Item and Tag relations.

Preparing the Model

The next step is to use these "specs" to initialize a PlistDatabase by loading existing data from disk or creating a new one. We wrap it in a TransactionalDatabase so that we can capture and apply snapshots. That class also has a handy saveOnTransactionEnd feature that, when enabled, gives us auto-save functionality. Finally, we wrap the TransactionalDatabase in an UndoableDatabase which will help us coordinate undoable/redoable operations:

class Model {
    let items: TransactionalRelation
    let tags: TransactionalRelation
    let itemTags: TransactionalRelation
    let selectedItemIDs: TransactionalRelation

    ...

    init(undoManager: PLRelationalBinding.UndoManager) {
        let specs: [Spec] = [
            Item.spec,
            Tag.spec,
            ItemTag.spec,
            SelectedItem.spec
        ]

        // Create a database or open an existing one (stored on disk using plists)
        let path = "/tmp/TodoApp.db"
        let plistDB = PlistDatabase.create(URL(fileURLWithPath: path), specs).ok!

        // Wrap it in a TransactionalDatabase so that we can use snapshots, and
        // enable auto-save so that all changes are persisted to disk as needed
        let db = TransactionalDatabase(plistDB)
        db.saveOnTransactionEnd = true
        self.db = db

        // Wrap that in an UndoableDatabase for easy undo/redo support
        self.undoableDB = UndoableDatabase(db: db, undoManager: undoManager)

        // Make references to our source relations
        func relation(for spec: Spec) -> TransactionalRelation {
            return db[spec.name]
        }
        items = relation(for: Item.spec)
        tags = relation(for: Tag.spec)
        itemTags = relation(for: ItemTag.spec)
        selectedItemIDs = relation(for: SelectedItem.spec)
    }
}

Wiring Up the User Interface

Now that we've laid out our primary relations, let's set our sights on building out the user interface.

In this article we will be focusing on the left side of the application, namely the "Add a to-do" field and the list of to-do items. (There is a lot to share about the implementation of the right side too, but in the interests of time and space we will save that for Part 2, coming soon.)

Click the "Play" button below to see this part of the application in action:

To keep things organized, I decided to break the application down into distinct functional requirements. Each requirement corresponds to a specific portion of the UI and explains how we want it to behave.

The following interactive screenshot shows the left half of the app in its completed state. Each red dot corresponds to a single functional requirement. Hover over a dot to read a summary of the requirement, then click to see how we implemented it using PLRelational.

1 2 3 4 5

In each of the sections that follow, we will show the relevant portions of code (really, a cross section of the Model, *ViewModel, and *View classes) that were used to implement a functional requirement. To explore the sources in Xcode, feel free to clone the PLRelational repository and follow along with the sources under Example Apps > TodoApp in the project.

REQ-1: New Item Field

A text field at top-left that allows the user to enter new to-do items. When the user types a non-empty string, a new pending item should be added at the top of the list, and the text field should be cleared.

Let's translate this requirement into some code. The first step is to define an undoable transaction in the Model that adds a single row to the items relation. We expose this as an ActionProperty in the ChecklistViewModel, and then set up a binding in ChecklistView:

class Model { ...
    /// Adds a new row to the `items` relation.
    private func addItem(_ title: String) {
        // Use UUIDs to uniquely identify rows.  Note that we can pass `id` directly
        // when initializing the row because `ItemID` conforms to the
        // `RelationValueConvertible` protocol.
        let id = ItemID()

        // Use a string representation of the current time to make our life easier
        let now = timestampString()

        // Here we cheat a little.  ArrayProperty currently only knows how to sort
        // on a single attribute (temporary limitation), we cram two things -- the
        // completed flag and the timestamp of the action -- into a single string of
        // the form "<0/1> <timestamp>".  This allows us to keep to-do items sorted
        // in the list with pending items at top and completed ones at bottom.
        let status = statusString(pending: true, timestamp: now)

        // Insert a row into the `items` relation
        items.asyncAdd([
            Item.id: id,
            Item.title: title,
            Item.created: now,
            Item.status: status,
            Item.notes: ""
        ])
    }

    /// Adds a new row to the `items` relation.  This is an undoable action.
    func addNewItem(with title: String) {
        undoableDB.performUndoableAction("Add Item", {
            self.addItem(title)
        })
    }
}

class ChecklistViewModel { ...
    /// Creates a new to-do item with the given title.
    lazy var addNewItem: ActionProperty<String> = ActionProperty { title in
        self.model.addNewItem(with: title)
    }
}

class ChecklistView { ...
    init() { ...
        // Add a new item each time a string is entered into the text field
        newItemField.strings ~~> model.addNewItem
    }
}

Note that the EphemeralTextField class in PLBindableControls takes care of delivering the text via its strings signal and clearing out the text field when the user presses enter.

Here we've established a pattern — breaking things down across the three components — that we'll see again and again in the implementation of our app. This layout encourages isolation between individual pieces of the UI and also allows for easy testing (a topic that we'll cover in a future article).

REQ-2: Items List

A list view on the left side that contains all to-do items. The list should be sorted such that the first part of the list contains pending items, and the second part contains completed items. Pending items should be sorted with most recently added items at top. Completed items should be sorted with most recently completed at top.

Table and outline views are a frequent source of headaches when building iOS and macOS applications. PLRelationalBinding and PLBindableControls include the ArrayProperty and ListView classes, respectively, that do a lot of heavy lifting so that we can focus simply on how the data is related. Here we use arrayProperty to lift the contents of our items relation into a form that can track rows by their unique identifier and keep them sorted by their status value:

class ChecklistViewModel { ...
    /// The model for the list of to-do items.
    lazy var itemsListModel: ListViewModel<RowArrayElement> = {
        return ListViewModel(
            data: self.model.items.arrayProperty(idAttr: Item.id,
                                                 orderAttr: Item.status,
                                                 descending: true),
            cellIdentifier: { _ in "Cell" }
        )
    }()
}

class ChecklistView { ...
    init() { ...
        // Bind outline view to the list view model
        listView = CustomListView(model: model.itemsListModel,
                                  outlineView: outlineView)
    }
}

The ListView class reacts to changes in the ArrayProperty, taking care of animating insertions and deletions (as seen in the animation at the top of this section). As you can see, it takes very little code to set this up; no custom NSTableViewDataSource or NSTableViewDelegate implementation required.

REQ-3: Item Cell Completed Checkbox

Each list cell will have a checkbox on the left side indicating whether the item is pending (unchecked) or completed (checked). If the user clicks the checkbox such that it becomes checked, the item should animate down the list to sit at the top of the completed section. If the user clicks the checkbox such that it becomes unchecked, the item should animate to the top of the list.

In the previous step, we used ArrayProperty to describe how the rows of to-do items are organized, but we still need to break each list cell down into three parts: checkbox, text field, and (tags) label.

For each of these cell components, we need to set up a conduit that takes data from a specific part of the underlying relation and delivers it to that part of the list cell.

Click the following to see an animation that helps visualize how, for each checkbox, we define a Property (in this case, an AsyncReadWriteProperty<CheckState>) that serves as a two-way transform:

Now, let's translate this into code. In Model we define a bidirectional transform that converts the checkbox state to our custom status string and vice versa. ChecklistViewModel creates an instance of that transform for a given Row (i.e., a list cell), and then in ChecklistView we bind the checkbox state to the view model:

class Model { ...
    /// Returns a property that reflects the completed status for the given relation.
    func itemCompleted(_ relation: Relation, initialValue: String?) -> AsyncReadWriteProperty<CheckState> {
        return relation.undoableTransformedString(
            undoableDB, "Change Status", initialValue: initialValue,
            fromString: { CheckState(parseCompleted($0)) },
            toString: { statusString(pending: $0 != .on, timestamp: timestampString()) }
        )
    }
}

class ChecklistViewModel { ...
    /// Returns a read/write property that resolves to the completed status for
    /// the given to-do item.
    func itemCompleted(for row: Row) -> AsyncReadWriteProperty<CheckState> {
        let itemID = ItemID(row[Item.id])
        let initialValue: String? = row[Item.status].get()
        let relation = self.model.items.select(Item.id *== itemID).project(Item.status)
        return self.model.itemCompleted(relation, initialValue: initialValue)
    }
}

class ChecklistView { ...
    init() { ...
        listView.configureCell = { view, row in ...
            // Bidirectionally bind checkbox state to the view model
            let checkbox = cellView.checkbox!
            checkbox.checkState.unbindAll()
            checkbox.checkState <~> model.itemCompleted(for: row)
        }
    }
}

There are a few things worth highlighting here:

  • In Model, our itemCompleted transform is set up using undoableTransformedString which provides built-in support for registering an action with the underlying UndoManager. UndoableDatabase will take care of reverting to the previous state if the user performs an "Undo" action, or reapplying the change after a "Redo" action.

  • In ChecklistViewModel, note how we use select and project to hone in an individual value in a specific row of a relation. We are effectively setting up a live connection to that value, but note that we never have to explicitly load or store an object.

  • In ChecklistView, the <~> operator means "set up a bidirectional binding between these two things": changes initiated by the user flow back to the model, and vice versa.

REQ-4: Item Cell Title Field

Each list cell will have the to-do item title to the right of the checkbox. The user should be able to change the title by clicking in the list cell's text field. The title field should be updated if the user changes it in the detail view, and vice versa.

The process we use to hook up the text field for each list cell is almost the same as what we did for the checkboxes in the previous step. Click to visualize:

Once again, let's translate this into code. The implementation of this requirement is very similar to the previous one, so it should all look familiar:

class Model { ...
    /// Returns a property that reflects the item title.
    func itemTitle(_ relation: Relation, initialValue: String?) -> AsyncReadWriteProperty<String> {
        return relation.undoableOneString(undoableDB, "Change Title", initialValue: initialValue)
    }
}

class ChecklistViewModel { ...
    /// Returns a read/write property that resolves to the title for the given
    /// to-do item.
    func itemTitle(for row: Row) -> AsyncReadWriteProperty<String> {
        let itemID = ItemID(row[Item.id])
        let initialValue: String? = row[Item.title].get()
        let relation = self.model.items.select(Item.id *== itemID).project(Item.title)
        return self.model.itemTitle(relation, initialValue: initialValue)
    }
}

class ChecklistView { ...
    init() { ...
        listView.configureCell = { view, row in ...
            // Bidirectionally bind title field to the view model
            let textField = cellView.textField as! TextField
            textField.string.unbindAll()
            textField.string <~> model.itemTitle(for: row)
        }
    }
}

The itemTitle transform is even simpler than the itemCompleted transform that we saw in the previous step. The undoableOneString convenience gives us a two-way (read/write) property that resolves to the title string value of the to-do item, and then writes updates back to the source relation when the user changes that string in the UI (again the undo support is handled for us).

REQ-5: Item Cell Tags Label

Each list cell will have a read-only label containing applied tags on the right side. The tags should be comma-separated and in alphabetical order. The label should be updated whenever the user adds or removes a tag for that item in the detail view.

For the third and final piece of our list cells, we will display a string representation of the list of tags applied to that item. Unlike the previous two steps (checkbox and text field), this one doesn't accept input from the user, so it's just a matter of creating a read-only property. Click to visualize:

Here's what that looks like in code form:

class Model { ...
    /// Returns a property that resolves to a string containing a comma-separated
    /// list of tags that have been applied to the given to-do item.
    func tagsString(for itemID: ItemID) -> AsyncReadableProperty<String> {
        return self.itemTags
            .select(ItemTag.itemID *== itemID)
            .join(self.tags)
            .project(Tag.name)
            .allStrings()
            .map{ $0.sorted().joined(separator: ", ") }
            .property()
    }
}

class ChecklistViewModel { ...
    /// Returns a property that resolves to the list of tags for the given
    /// to-do item.
    func itemTags(for row: Row) -> AsyncReadableProperty<String> {
        return self.model.tagsString(for: ItemID(row))
    }
}

class ChecklistView { ...
    init() { ...
        listView.configureCell = { view, row in ...
            // Bind detail (tags) label to the view model
            let detailLabel = cellView.detailLabel!
            detailLabel.string.unbindAll()
            detailLabel.string <~ model.itemTags(for: row)
        }
    }
}

It's worth dissecting that tagsString declaration; it's an interesting one. Let's break it down step by step:

// Start with the `itemTags` source relation (Item.id : Tag.id pairs)
self.itemTags

// Select the rows corresponding to the given to-do item
.select(ItemTag.itemID *== itemID)

// Join with the `tags` relation to get the tag names
.join(self.tags)

// Take just the tag names (we can drop the IDs)
.project(Tag.name)

// Derive a `Signal` that carries the tag names as a `Set<String>`
.allStrings()

// Convert the `Set<String>` into a sorted, comma-separated string
.map{ $0.sorted().joined(separator: ", ") }

// Lift the `Signal` into an `AsyncReadableProperty` that offers the latest value
.property()

This demonstrates a common practice when working with PLRelational:

  • Start with one or more Relations
  • Slice and dice (i.e., select, join, and project) until you've narrowed your focus onto a small set of data
  • Derive a Signal that extracts Swift-ly typed values
  • Transform the values using Signal operators like map
  • Lift to an AsyncReadableProperty for easier binding (and unit testing)

By declaring the relationships in this manner, any time the list of tags changes for that to-do item (by virtue of the user adding/removing tags, or as a result of undo/redo, etc), the list cell will automatically be updated to display the latest string value.

Wrapping Up

In this article, we demonstrated how to use PLRelational and its reactive-relational style of programming to build a real, working macOS application.

Building the PLRelational Way takes a slightly different mindset as compared to more imperative, object-oriented approaches. However, once you've learned the few core patterns that we presented here, the reactive-relational approach becomes second nature, and using the vocabulary offered by PLRelational can help simplify the way you build applications.

In Part 2, we will continue our deep dive and explore the right half of the to-do application. In that article we will cover topics such as modeling list selection, building with complex controls like combo boxes, using cascading deletion, and more.

The complete source code for this application is available in the PLRelational repository under the Examples/TodoApp directory and can be run directly from the Xcode project.

The Best New Features in Swift 4

September 13, 2017, by Mike Ash

Swift 4 is here, and it's bringing some nice changes. We're not getting a radical rework of the syntax like we did last year, nor are we getting a breathtaking pile of new features like we did for Swift 2, but there are some nice additions you can use to improve your code. Let's take a look!

Multi-Line String Literals

Sometimes you want long, multi-line strings in your code. It might be an HTML template, a blob of XML, or a long message for the user. Either way, they're painful to write in Swift 3.

You can write them out all on one line, which gets ugly fast:

let message = "Please disable your Frobnitz before proceeding.\n\nTo do this, visit Settings -> Frobnitz, then toggle the switch to \"off\".\n\nIf you need the Frobnitz to remain enabled, tap \"Proceed Anyway\" below."

You can split it onto multiple lines by concatenating strings:

let message =
    "Please disable your Frobnitz before proceeding.\n\n"
  + "To do this, visit Settings -> Frobnitz, then toggle the switch to \"off\".\n\n"
  + "If you need the Frobnitz to remain enabled, tap \"Proceed Anyway\" below."

There are other ways to do it too, but none of them are all that good.

Swift 4 solves this problem with multi-line string literals. To write a multi-line string literal, use three quote marks at the beginning and end:

let message = """
    Please disable your Frobnitz before proceeding.

    To do this, visit Settings -> Frobnitz, then toggle the switch to "off".

    If you need the Frobnitz to remain enabled, tap "Proceed Anyway" below.
    """

If you've used Python, then this new syntax will look familiar. However, it's not quite the same. There are some interesting limitations and features of this syntax in Swift.

This triple-quote syntax cannot be used on a single line. Something like the following will not compile:

// Will not compile
label.text = """Put your text in "quotes" to make them look quoted."""

This could be handy to avoid having to escape quotes, but it's not allowed. The content of the string must be on separate lines between the """ marks.

Multi-line strings can be indented in your code without indenting the final result. The multi-line string above indents each line in the code, but the string placed into message has no leading whitespace. This is really nice, but what if you want some indentation? This feature is based on the indentation of the closing """ mark. Its indentation will be stripped off all of the other lines. If for some reason you needed the contents of message to be indented, you can do so by indenting the text farther than the closing """ mark:

let message = """
        Please disable your Frobnitz before proceeding.

        To do this, visit Settings -> Frobnitz, then toggle the switch to "off".

        If you need the Frobnitz to remain enabled, tap "Proceed Anyway" below.
    """

To avoid confusion, each line of text must be indented at least as much as the closing """ mark. A line with less indentation will produce an error.

You may want to split your text onto multiple lines without producing multiple lines in the output. You can remove a line break from the resulting string by adding a \ at the end of the line:

let message = """
    Please disable your Frobnitz before proceeding. \
    To do this, visit Settings -> Frobnitz, then toggle the switch to "off". \
    If you need the Frobnitz to remain enabled, tap "Proceed Anyway" below.
    """

One-Sided Ranges

This is a nice, small change that's mostly self explanatory. Ranges can now be one-sided, and the "empty" side is implied to be the minimum or maximum value that makes sense in context.

When subscripting a container, this means you can leave off things like string.endIndex or array.count. For example, if you want to split an array into halves:

let middle = array.count / 2
let firstHalf = array[..<middle]
let secondHalf = array[middle...]

Or if you want to get a substring up to a particular index:

let index = string.index(of: "e")!
string[..<index]

It can also be handy in switch statements:

switch x {
case ..<0:
    print("That's a negative.")
case 0:
    print("Nothing!")
case 1..<10:
    print("Pretty small.")
case 10..<100:
    print("Bigger.")
case 100...:
    print("Huge!")
default:
    // Unfortunately, the compiler can't figure out
    // that the above cases are exhaustive.
    break
}

For one-sided ranges up to a given value, you can use ..< for an exclusive range or ... for an inclusive range, just like two-sided ranges. For one-sided ranges starting at a given value, only ... is allowed, since the distinction between ... and ..< makes no sense there.

Combined Class and Protocol Types

Sometimes you need an object which both subclasses a class and conforms to a protocol. For example, you might need a UITableViewController that also implements KittenProvider. Swift 3 had no way to express this idea, requiring various ugly workarounds. Interestingly, Objective-C is able to express this idea:

UITableViewController<KittenProvider> *object;

Swift 4 can now express this concept as well by using the & symbol. This could already be used to combine multiple protocols into a single type, and now it can also be used to combine protocols with a class:

let object: UITableViewController & KittenProvider

Note that only one class can be included in any such type, since you can't subclass more than one class at a time anyway.

Generic Subscripts

Swift has supported generic methods forever, but before Swift 4 it did not support generic subscripts. You could overload subscripts by implementing more than one with different types, but you couldn't use generics. Now you can!

subscript<T: Hashable>(key: T) -> Value?

Generics are fully supported, so you can use things like where clauses:

subscript<S: Sequence>(key: S) -> [Value] where S.Element == Key

Just like methods, the generic type can be used as a return value as well:

subscript<T>(key: Key) -> T?

This could be really handy for dynamically-typed containers, such as when dealing with JSON objects.

Codable

Speaking of JSON, perhaps the biggest new feature in Swift 4 is the Codable protocol. The compiler will now auto-generate serialization and deserialization code for your types, and all you have to do is declare conformance to Codable.

Imagine you have a Person type:

struct Person {
    var name: String
    var age: Int
    var quest: String
}

If you wanted to read and write Person values to and from JSON, you previously had to write a bunch of annoying repetitive code to do so.

In Swift 4, you can make this happen by adding half a line:

struct Person: Codable {

If for some reason you only want to support encoding or decoding, but not both, you can declare conformance to Encodable or Decocable separately:

struct EncodablePerson: Encodable { ... }

struct DecodablePerson: Decodable { ... }

Conforming to Codable is just a shortcut for conforming to both.

Using a Codable type requires an encoder or decoder, which determines the serialization format and how Swift values are translated to and from serialized values. Swift provides encoders and decoders for JSON and property lists, and Foundation's archivers also support Codable types.

To encode something as JSON, create a JSONEncoder and call its encode method:

let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(person)

To decode, create a JSONDecoder and call decode, passing it the type you want to decode and the data to decode from:

let jsonDecoder = JSONDecoder()
let decodedPerson = try jsonDecoder.decode(Person.self, from: data)

Note that encoding and decoding methods are marked as throws because there are a lot of potential errors that can occur during the process, such as type mismatches or incomplete data, so you'll need to add try to these calls and catch the errors they throw.

Since JSON doesn't natively support dates or binary data, those values need to be converted to/from some other JSON representation. For example, it's common to use base64 encoding for data. JSONEncoder and JSONDecoder can be customized with different strategies for handling these values. For example, if you want to encode dates as ISO-8601 and data as base64:

let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .iso8601
jsonEncoder.dataEncodingStrategy = .base64
let data = try jsonEncoder.encode(person)

And on the decode side:

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601
jsonDecoder.dataDecodingStrategy = .base64
let decodedPerson = try jsonDecoder.decode(Person.self, from: data)

They also provide the option for providing a totally custom strategy by writing your own code.

Property list coding is similar. Use PropertyListEncoder and PropertyListDecoder instead of the JSON coders. Since property lists can natively represent dates and binary data, the property list coders don't provide those options.

If you're already using NSCoding then you can mix and match it with Codable so that you don't have to change everything at once. NSKeyedArchiver provides a new encodeEncodable method which takes any Encodable type and encodes it under the given key. NSKeyedUnarchiver provides a corresponding decodeDecodable which can decode any Decodable type.

Codable is a flexible protocol with lots of room for custom behavior. The compiler provides a default implementation, but you can provide your own if you need different behavior. This makes it straightforward to write implementations that migrate old data into new types, use different names in the serialized representation than in the source code, or make other customizations.

A full discussion of all the possibilities of Codable is beyond the scope of this article, but you can read more about it in my Friday Q&A post about Swift.Codable.

Wrapping Up

Swift 4 hasn't brought us the dramatic changes we've seen in earlier years, but it's a solid improvement. Codable will make some really common tasks a lot easier, and it's probably my favorite feature out of the bunch. Other features like multi-line string literals and generic subscripts won't have the same impact, but put together they should make for nice improvement in the code we write.