Better Strategies Through Types
Figuring Out What Not to Do
Imagine we’ve written a UIControl
that wraps a button to do some animations whenever it’s tapped:
class BouncyButton: UIControl {
private weak var button: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
do {
let b = UIButton(type: .system)
addSubview(b)
button = b
}
button.addTarget(self,
action: #selector(handleTap),
for: .touchUpInside)
// Set frame or constraints...
}
@objc func handleTap() {
//Do some bouncy animations here...
sendActions(for: .touchUpInside)
}
}
We could hardcode our animation in the handleTap()
method, but maybe we need our button to bounce different ways in different contexts. Encapsulating the behavior of a thing in a swappable container is a common enough practice in software design that it has its own pattern — the strategy pattern.
In Cocoa, strategies have traditionally be implemented via delegates:
protocol BouncyDelegate {
func animateBounce(for view: UIView)
}
class BouncyButton: UIControl {
let delegate: BouncyDelegate //🤔
//...
init(frame: CGRect, delegate: BouncyDelegate) {
self.delegate = delegate
//...
}
@objc func handleTap() {
delegate.animateBounce(for: button)
sendActions(for: .touchUpInside)
}
}
This seems straight-forward enough, but there’s a problem. The delegate is strongly held by our control. You can imagine a view controller with this button in its hierarchy setting itself as the button’s delegate. Then the view controller would own the button, which owns the delegate, which owns the button, which owns… a retain cycle!
For this reason, the Cocoa convention is to make all delegates weak
. No problem:
weak let delegate: BouncyDelegate
//🛑: 'weak' must be a mutable variable
Oh, I mean:
weak var delegate: BouncyDelegate
//🛑: 'weak' may not be applied to
// non-class-bound protocol
Hmm. We can fix1 this with:
protocol BouncyDelegate: class { ... }
But this will prevent us from using struct
s or enum
s to implement our strategy and, at this point, ought make us a little suspicious of what’s going on.
Something Rotten in the State
We have to limit our delegate to class
implementations because delegates are assumed to hold mutable state.
Since they have state, they need to be instantiated. If they need to be instantiated, we have to provision storage for them. If we have to provision storage for them, questions around ownership need to be resolved. Qualifiers like weak
and (the implicit) strong become necessary.
If we could somehow define our strategies to be stateless things, all of this would cease to be relevant.
But moreover, the thought of mixing state and strategies ought to give us the heebie-jeebies.2 Storing state means a strategy might behave differently depending on the values kept within it. Unless we’re writing a parser, no one wants to have think about every other thing that might have mutated our delegate before us and in what order.3
So we want to do away with state in our strategies. And if we’re going to do away with state, then instances are nothing more than chunks of memory we don’t use but still need to manage ownership of. We should get rid of those, too.
Type Method Acting
So rather than holding our strategy’s implementation in instance methods that need to be instantiated, we’re going to move it all up into type methods on the type.
Let’s start with the protocol:
protocol BouncyDelegate {
static func animateBounce(for view: UIView)
}
The static
here indicates a type conforming to BouncyDelegate
must have animageBounce(for:)
defined on its type. Anything can technically conform to this, but I like to use enum
s because they can never be instantiated, even accidentally.
enum ShakeStrategy: BouncyDelegate {
static func animateBounce(for view: UIButton) {
let base = CGAffineTransform.identity
let offset = base.translatedBy(x: 30, y: 0)
view.transform = offset
UIView.animate(withDuration: 0.5, delay: 0,
usingSpringWithDamping: 0.2,
initialSpringVelocity: 0,
options: [],
animations: { view.transform = base }
completion: nil)
}
}
Great! We have our strategy type defined. Now we have to change our button a bit to use it:
class BouncyButton: UIControl {
let delegate: BouncyDelegate.Type
//...
init(
frame: CGRect,
delegate: BouncyDelegate.Type) {
self.delegate = delegate
//...
}
@objc func handleTap() {
delegate.animateBounce(for: button)
sendActions(for: .touchUpInside)
}
}
Note we’ve had to change the type of delegate
from BouncyDelegate
, which would be an instance of a type conforming to our protocol, to BouncyDelegate.Type
, which is the conforming type, itself.
We’ll initialize our button by passing in our strategy type like so:
let button = BouncyButton(
frame: someRect,
delegate: ShakeStrategy.self)
Note the use of .self
to indicate we want to use the type itself as our delegate.
Generic Brand Awareness
If all this .Type
and .self
stuff feels a little awkward, it’s probably because Swift already supports this kind of thing as a language feature. It has a specific syntax just for passing around types that are used to specialize implementations. We know it as “generics”.
We can rewrite our button like so:
class BouncyButton<Strategy>: UIControl
where Strategy: BouncyDelegate {
//...
init(frame: CGRect) {
//...
}
@objc func handleTap() {
Strategy.animateBounce(for: button)s
sendActions(for: .touchUpInside)
}
}
Note that not only do we get to drop the ownership qualifiers around our delegate
property, we don’t need a delegate
property at all! This, in turn, simplifies our init
and ditches all the confusing .Type
stuff.4
The .self
stuff disappears, too. We now create our button like so:
let button =
BouncyButton<ShakeStrategy>(frame: someRect)
Cool! But a further, easy-to-overlook benefit is how we’ve moved responsibility for specifying strategy away from init params and into the type. Now if we ever find ourselves allergic to angle brackets5 we can alias the whole thing away with another of Swift’s type-related language features:
typealias ShakyButton = BouncyButton<ShakeStrategy>
let button = ShakyButton(frame: someRect)
1: Almost. We’ll still have to make BouncyDelegate
optional, as well.↩︎
2: The Cocoa convention of passing delegate methods the operative object as their first parameter (i.e. tableView(_:heightForRowAt:)
) could be seen as a suggestive push in the direction of stateless delegation.↩︎
3: And those writing parsers wish they didn’t have to.↩︎
4: Though, as Michael Tsai points out, “unlike delegates, the type cannot change at runtime.” Dynamism is a key feature not only of delegates, but also of the strategy pattern, generally. Be aware that once we move to generics, we’re giving that up (though we can still swap out types with .self
at runtime, as per the previous example).↩︎
5: Or if we’re shipping a framework and want to, say, restrict 3rd party customization of our controls beyond certain pre-defined combinations of strategies…↩︎