Custom Keyboard View with Bindings

The code for this is at GitHub.

If you run it you will see that you can select/unselect keys, singly or with click-drag. Undo works. Save works. The “XYZ” label does nothing currently.

keyboardviewbindings

This project has a number of features I’m trying to adopt as standards-of-a-sort for my projects:

  • Document manages data as instance properties (in this case [Key])
  • Document creates Window Controller, which creates main View Controller
  • Project uses dependency injection into controllers’ representedObject to constrain access, and to genericize this crucial element of app architecture (not that relevant in this case, where there’s only one view controller and simple data)
  • Dealing with the sequence of events that can cause difficulties when wiring up bindings and observers — the relevant sequence of calls is (for this style of app architecture YMMV):
    • Document::makeWindowControllers
    • Window Controller::windowDidLoad
    • Custom View::init
    • View Controller::viewDidLoad
    • View Controller::finalSetup (my own method, called by Window Controller)

I’m still not sold on storyboards (for MacOS projects. Have not done any iOS storyboard projects so have no opinion there). Maybe I will dabble around with them some more, but having individual XIB files (and controllers) has in general worked more smoothly for me.

So, some details…

The Document class takes care of:

  • Archiving/Loading
  • KVO
  • Undo (some of this probably would be handed off to view controllers for multiple view controller projects)

The Window Controller class takes care of:

  • Instantiating primary view controller
  • Injecting the document data as representedObject into the view controller
  • Assigning the view controller to the window controller’s contentViewController
  • Calling the view controller’s finalSetup method (where the binding and observering will happen)

Here’s the relevant window controller code:

	override func windowDidLoad() {
		super.windowDidLoad()

		let viewController = KVBViewController()
		let doc = self.document as! Document
		viewController.representedObject = doc.keys

		// It's critical that representedObject be set up
		// before assigning contentViewController, or
		// bindings won't be set up correctly.
		contentViewController = viewController

		viewController.finalSetup()
 	}

It’s useful to point out that the act of assigning viewController to contentViewController is what causes the custom view to be intantiated and the viewDidLoad method to be called.

I have a finalSetup routine because when the view is instantiated before viewDidLoad is called (duh), so if bindings need to happen in the view this has to wait at least to the end of viewDidLoad(). I’m currently questioning the logic of breaking this into two pieces though…

The View Controller class takes care of:

  • Dealing with bindings and observation WRT the custom view.

If IB supports @IBInspectable in Swift, I haven’t figured out how to get it to work. In this case, we need to bind the view’s “keys”, “selection” and “selected” properties to the associated array controller. Most of this code is a modification of one of Apple’s demo projects, illustrating how to support bindings in a custom view. I just went looking for it and failed. There is, however, another one I hadn’t seen before — BindingsJoystick.

The Custom View (KeyboardView) class takes care of:

  • Mouse tracking
  • Drawing
  • The guts of the Observation and Binding code.

I know I’m very sloppy with initializers. There’s some refactoring that should be done.

There’s some math involved with drawing the keys. If I remember right, you need to be careful about having the view’s frame be calculated carefully so that you don’t get weird interpolation artifacts when the view is rendered. Probably fixable, but that’s not the point of this project.

The Key class takes care of:

  • encoding/decoding itself
  • declaring what properties are observable, which brings us to…

The ObservablePropertiesListing protocol takes care of:

  • Exposing what observable properties the class wishes to expose
  • Ensuring that addObserver/removeObserver are available.
protocol ObservablePropertiesListing {
	func observableProperties() -> [String]
	
	func addObserver(_ observer: NSObject,
	                 forKeyPath keyPath: String,
	                 options: NSKeyValueObservingOptions,
	                 context: UnsafeMutableRawPointer?)
	func removeObserver(_ observer: NSObject,
	                    forKeyPath keyPath: String,
	                    context: UnsafeMutableRawPointer?)
}

Combined in the same file with its supporting NSObject extension:

The ObservablePropertiesListing protocol takes care of:

  • Supplies the startObservingObject/stopObservingObject methods.
extension NSObject {
	func startObservingObject(item: ObservablePropertiesListing,
	                          context: UnsafeMutableRawPointer?) {
		for keyName in item.observableProperties() {
			item.addObserver(self, forKeyPath: keyName,
			                 options: .old, context: context)
		}
	}
	func stopObservingObject(item: ObservablePropertiesListing,
	                         context: UnsafeMutableRawPointer?) {
		for keyName in item.observableProperties() {
			item.removeObserver(self, forKeyPath: keyName,
			                    context: context)
		}
	}
}

The best example of the use of this protocol and extension is in Document.swift:

	func addObservers() {
		if !isObserving {
			for key in keys {
				startObservingObject(item: key, context: &documentContext)
			}
			isObserving = true
		}
	}
	func removeObservers() {
		if isObserving {
			isObserving = false
			for key in keys {
				stopObservingObject(item: key, context: &documentContext)
			}
		}
	}

The document doesn’t need to know anything about the particulars of what properties are observable. The key class publishes this info like so:

	func observableProperties() ->  [String] {
		return [
			selectedKeyName,
		]
	}

Of course, I could have exposed more properties than that. If you look at Key.swift you will see that I have more complex uses for the Key object planned.

I’ve found this ObservablePropertiesListing pattern to be very useful — particularly with mult-ViewController projects.

Leave a Reply

Your email address will not be published. Required fields are marked *