Composition Over Inheritance in NSViews
I find myself looking at an NSView
subclass that actually doesn’t do much.
It’s an opaque container for a text field that sets a label text. “Opaque” inasmuch as its outward appearance is being view, hiding the actual component’s complexity inside.
final class FileOutputLabel: NSView {
var settings: Settings? = nil {
didSet {
updateLabelText()
}
}
private let textField: NSTextField
required init() {
self.textField = NSTextField(wrappingLabelWithString: "")
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.textField)
self.textField.translatesAutoresizingMaskIntoConstraints = false
self.textField.addConstraintsToFillSuperview() // Sets 4 directional anchors without edge insets.
updateLabelText()
}
@available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func updateLabelText() {
textField.stringValue = settings?.fileTarget.labelText
}
}
This is just one example of an oft-repeated pattern: delegate to sub-views, using view composition instead of inheritance. This reduces the public API quite a bit by virtue of its opaque nature.
I can think of a couple of approaches to get some data on screen in such a way:
- Get the string value of the data in a view controller, and pass it to a
textField.stringValue
. The controller needs to know the text field (and all other detail views) and configure them in a way that they understand. Knowledge is bundled in the view controller. - Subclass the view types needed, and offer a unified
update(model:)
(maybe require this as a formal protocol) to update all of them. The view controller then can treat all these view components the same and callupdate(model:)
in a loop. - Instead of subclassing, wrap the views you need in a container view and offer a
update(model:)
on that. This container needs to know how to forward the data to its subcomponents. The view controller can still treat all containers the same. But on top, the view controller doesn’t have access to any particular view APIs – labels and table views and images are all hidden away. - Bonus: The same approach as (3), but instead of offering plain views that hide concrete components like labels, table views, and images in the view hierarchy, offer view controllers and compose these.
This is all very basic programming stuff: offer a function on the object (the view) that accepts new data as it changes, then have the view change its internals to reflect the change.
The thing that stood out to me here that made it noteworthy is that I called the container view a “label”, while the actual text field is hidden inside. You don’t get the standard AppKit label API, but as a visible thing on screen, it does behave like a label.
Maybe nowadays I need to point this out: in AppKit, labels are actually non-editable NSTextField
’s – unlike with SwiftUI, where you have a Text
view to display text, and a TextField
to enter text. This (admittedly still kind of weird) pecularity of AppKit is completely hidden away in the type above.
With a subclass of NSTextField
instead, it wouldn’t have been possible to hide this fact. Client code could make it editable, for example. And all the editable text field API would be exposed as part of the view component’s surface area.
In general, controls (NSControl
subclasses) have a much larger surface area than mere views to handle all kinds of user interaction. That’s great for first party API because we app developers get a lot of knobs to turn to tune everything. It’s also quite noisy.
Now this dumb NSView
container hides the complexity of AppKit’s text field API, editable or non-editable, inside its own subviews
collection.
As a consequence, composing a small sub-view hierarchy like this for every component you creare (as apposed to inheriting from e.g. NSTextField
), the view hierarchy will get deeper. Do the thing above for every N
labels on screen, and you have 2N
views in the view hierarchy. But the actual overhead of having empty views in the view hierarchy is mostly limited to the cost of tree traversal, which is negligible, so I don’t mind this trade-off.
With SwiftUI, we’re nudged to do something like this all the time, and it’s not only natural, it’s the only sane way to write UI. We add custom SwiftUI.View
types that have a computed body
property that in turn contains the components we want to compose. To client code, details about the view composition and modifiers are invisible.
Over the years, I noticed that my AppKit code adheres to a couple of peculiar conventions. This is one of those. I believe I merely transferred some general programming and object-oriented design practice to the AppKit world: it’s nothing special, but it’s not the convention prevalent in sample code of the Objective-C era as far as I can tell. (Which isn’t very far, I admit.) And I do notice that with SwiftUI, a similar practice is becoming the norm. I guess all I want to say is that you don’t have to limit yourself to benefitting from this useful practice in SwiftUI projects. You can have the same kind of fun in AppKit and UIKit, too.
I suggest you try it if you haven’t, yet. It’s one more hop when you navigate around the code, but it offers you a bit more flexibility in view composition.