I have been working on porting over my ObjC platform layer from
Handmade Hero and I have a bit of code that looks like this:
self.aboutMenuItem.title = [self.aboutMenuItem.title
stringByReplacingOccurrencesOfString:DEADZONE_TITLE
withString:APP_NAME];
self.hideMenuItem.title = [self.hideMenuItem.title
stringByReplacingOccurrencesOfString:DEADZONE_TITLE
withString:APP_NAME];
self.quitMenuItem.title = [self.quitMenuItem.title
stringByReplacingOccurrencesOfString:DEADZONE_TITLE
withString:APP_NAME];
self.helpMenuItem.title = [self.helpMenuItem.title
stringByReplacingOccurrencesOfString:DEADZONE_TITLE
withString:APP_NAME];
self.window.title = [self.window.title
stringByReplacingOccurrencesOfString:DEADZONE_TITLE
withString:APP_NAME];
The point of this code is to go through and replace all of the text with the
target name in it to a string defined by a config file. But you can easily
imagine that this is any code that is starting to look fairly redundant across
a set of various differently typed items.
This is really just a simple case of iterating over the items in the collection and calling the title
setting.
for (var item) in items {
item.title = item.title.replace(placeholder, withString: title)
}
The astute readers out there may have caught an issue with this approach though. For one, a window
is of type NSWindow
and the other items are of type
NSMenuItem
. That's not going to work as they share no base class that contains a title
property.
We have two choices here:
- Handle
window
on it's own separate line, or 2. Create a protocol extension so they do have a common interface with atitle
property.
I want my code to capture the intent as much as possible with as little redundancies as possible, so option #1 is out.
So, let's think about option #2. What I really want to say is that both
NSWindow
and NSMenuItem
share a common set of behaviors, or more specifically in this case, a single behavior: title
.
Now, it seems that they actually already do share this behavior and that it is just not codified. However, if you look at the declaration of each, you'll notice that NSWindow
actually returns a String?
for title
and NSMenuItem
returns a String
(note the optional vs. non-optional difference).
Ok… that's annoying, but we can work around that!
protocol HasTitle {
var ht_title: String { get set }
}
extension NSMenuItem: HasTitle {
var ht_title: String {
get { return self.title }
set { self.title = newValue }
}
}
extension NSWindow : HasTitle {
var ht_title: String {
get { return self.title ?? "" }
set { self.title = newValue }
}
}
With the protocol extension HasTitle
, we have now codified what the behavior semantics are. It's then trivial to implement that appropriately for each type that you want to share this HasTitle
codification.
The final code to make use of this now looks like:
let items: [HasTitle] = [aboutItem, hideItem, quitItem, helpItem, window]
for (var item) in items {
item.ht_title = item.ht_title.replace(placeholder, withString: title)
}
I almost forgot… you'll probably paste this code in and wonder why it is not
working for you. I've created an extension on
String
that adds thereplace
function. It simply forwards the parameters into
stringByReplacingOccurrencesOfString
.
Now, unfortunately, Swift needed a little bit of type annotation help to make it all work 100%, but we've done it. With one simple protocol and two easy class extensions, our usage code has become significantly cleaner, easier to update, and less error prone.
Could we bring this technique back to ObjC? Of course. Just write the simple for-loop to begin with. However, you'll probably stop there because that is all that is needed to make the ObjC side happy as we can send messages to any object instance; we can completely forgo the HasTitle
protocol.
However, that is the part that I think it vastly more important as it's that protocol that leads us into thinking about better composition models. I hope to do a longer blog post about that at another time.
Happy Swifting!
Update, February 16th: I removed the usage1 of map
as it was an abuse of the function. I used it because I didn't want to use the ugly version of the for-loop and I was having issues with the iterative version. Turns out I just need (var item)
instead of var item
. Seems like a compiler bug to me…
- Ok… it was really a very bad abuse of the map() function. ↩