NSTokenField is particularly tricky to use with Core Data bindings. I wanted to model a to-many relationship between an object (specifically Client, Project, etc.) and multiple Tags. The user would select e.g. a client from a table view, and in the inspector panel a token field would represent the assigned tags, which the user can edit. It’s a typical master-detail configuration. Tags that already exist are autocompleted; non-existent tags are created once the focus leaves the field. When the user removes tags from the field, they are also automatically removed from the relationship by Core Data.

As the default behaviour of NSTokenField is not going to suffice, and I’m going to be using tag fields fairly often in my project, a little bit of code is needed:

import Cocoa

 

protocol TagsFieldController {

    var tagsTokenField: NSTokenField! { get set }

    var completionsArrayController: NSArrayController! { get set }

 

    func autocomplete(substring: String) -> [String]

    func assignTags(to managedObject: NSManagedObject)

}

 

extension TagsFieldController {

    func autocomplete(substring: String) -> [String] {

        guard let completionsArray = completionsArrayController.arrangedObjects as? NSArray else {

            return []

        }

 

        guard let tokens = tagsTokenField.objectValue as? NSArray else { return [] }

        // Get trimmed text from field.

        let token = substring.trimmingCharacters(in: CharacterSet(charactersIn: " "))

        

        // Remove from completions array tokens already selected.

        let filteredCompletions = completionsArray.filter({(item: Any) -> Bool in

            guard let tag = item as? Tag else { return false }

            return !tokens.contains(tag)

        }) as NSArray

        

        // Select words starting with what the user entered.

        let predicate = NSPredicate(format: "SELF.name beginswith[cd] %@", token)

        let completions = filteredCompletions.filtered(using: predicate).map({ (item) -> String in

            // Replace first character of tag name with the first character the user typed,

            // to preserve case in the field.

            return String(substring.first!) + String((item as! Tag).name!.dropFirst())

        })

        return completions

    }

    

    func assignTags(to managedObject: NSManagedObject) {

        precondition(managedObject.entity.toManyRelationshipKeys.contains("tags"))

        

        // Ensure all tags exist

        var tags: [Tag]?

        

        if let tagArray = tagsTokenField.objectValue as? [String] {

            let tagStore = TagStore()

            tags = tagStore.ensureAllExist(tags: tagArray)

            if tags != nil {

                tagStore.assignTags(to: managedObject, tags: tags!)

            }

        }

    }

}

I’ll use this protocol to extend my View Controller, which is also the token field delegate:

import Cocoa

 

class TagsTabViewController: NSViewController, TagsFieldController, ObjectStoreManager {

    // MARK: - Outlets

    

    @IBOutlet var arrayController: NSArrayController!

    @IBOutlet var tagsTokenField: NSTokenField!

    @IBOutlet var completionsArrayController: NSArrayController!

    

    // MARK: - Properties

    

    private lazy var tagStore: TagStore = TagStore()

    @objc dynamic lazy var managedContext: NSManagedObjectContext! = tagStore.managedContext

    

    var dataController: NSObjectController! {

        return arrayController

    }

    

    // MARK: - Function Overrides

 

    deinit {

        arrayController.unbind(.contentArray)

        arrayController.unbind(.selectionIndexes)

    }

}

 

// MARK: - Extensions

 

extension TagsTabViewController: NSTokenFieldDelegate {

    func tokenField(_ tokenField: NSTokenField,

                    completionsForSubstring substring: String,

                    indexOfToken tokenIndex: Int,

                    indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {

        return autocomplete(substring: substring)

    }

    

    func tokenField(_ tokenField: NSTokenField,

                    displayStringForRepresentedObject representedObject: Any) -> String? {

        let tag = representedObject as? Tag

        return tag?.name

    }

    

    func tokenField(_ tokenField: NSTokenField,

                    editingStringForRepresentedObject representedObject: Any) -> String? {

        let tag = representedObject as? Tag

        return tag?.name

    }

    

    func tokenField(_ tokenField: NSTokenField,

                    representedObjectForEditing editingString: String) -> Any? {

        return tagStore.fetchByName(name: editingString)

    }

    

    func tokenField(_ tokenField: NSTokenField,

                    shouldAdd tokens: [Any],

                    at index: Int) -> [Any] {

        let tags = tagStore.ensureAllExist(tags: tokens)

        return tags

    }

}

Before I get to bind the token field, I’ll need the necessary Array Controllers configured to access clients and tags.

Configuring the Array Controllers

Once a client is selected in the table view, a Clients Array Controller will be bound to that selection so all properties of the selected object are available for binding.

The Tags Array Controller will be used to provide content to the token field for the selected client. As the tags relationship for a client is an NSSet, the Content Set property has to be bound to the Clients Array Controller with “selection" as the Controller Key and “tags" as Model Key Path.

 

 

Finally the Completions Array Controller will be used to access all tags to support token field autocomplete. There’s no special configuration for this controller, so it's left as is.

Value Transformer for NSSet

Because the NSTokenField stores tokens in an NSArray and the tags relationship is an NSSet, we need a custom ValueTransformer to convert between each other:

import Foundation

import Cocoa

 

class TagSetTransformer: ValueTransformer {

    override class func transformedValueClass() -> AnyClass {

        return NSArray.self

    }

    

    override class func allowsReverseTransformation() -> Bool {

        return true

    }

    

    override func transformedValue(_ value: Any?) -> Any? {

        guard let value = value else { return nil }

        

        if let set = value as? NSSet {

            return set.allObjects

        } else {

            return NSArray(object: value)

        }

    }

    

    override func reverseTransformedValue(_ value: Any?) -> Any? {

        guard let array = value as? NSArray else { return nil }

        return NSSet(array: array as! [Any])

    }

}

 

extension NSValueTransformerName {

    static let tagSetTransformerName = NSValueTransformerName(rawValue: "TagSetTransformer")

}

Here the TagSetTransformer will automatically create Tag objects replacing string tokens as internal representation. Core Data will then create the relationship with the parent object.

Configuring the Token Field

At last, binding the token field depends on the current client selection and the TagSetTransformer.

Conclusion

Although the solution I implemented is a little more involved than usual when using other controls with Core Data, it proves to be effective for my purpose. I tried to abstract away much of the work into the TagsFieldController protocol and the TagSetTransformer, even though some of the code smells a bit. Ultimately it works as expected.