In a language like JavaScript, configuration objects (or, as they’re known by JS convention, options
), are just a dictionary. Default options are merged into the dictionary using a function like Underscore.js’s _.defaults
.
Because of Swift’s type system, typeless dictionaries aren’t as nice to use as they are in a more dynamic language like JavaScript. Structs, on the other hand, make for great configuration objects. Unfortunately, configuring those configuration objects is sometimes a bit unweildy. Let’s take a look at the data for a form’s text field.
struct FieldData {
let title: String
let placeholder: String
let keyboardType: UIKeyboardType
let secureEntry: Bool
let autocorrectType: UITextAutocorrectionType
let autocapitalizationType: UITextAutocapitalizationType
}
This struct will give us an implicit “memberwise” initializer, but let’s write it explicitly.
init(title: String, placeholder: String, keyboardType: UIKeyboardType, secureEntry: Bool, autocorrectType: UITextAutocorrectionType, autocapitalizationType: UITextAutocapitalizationType) {
self.title = title
self.placeholder = placeholder
self.keyboardType = keyboardType
self.secureEntry = secureEntry
self.autocorrectType = autocorrectType
self.autocapitalizationType = autocapitalizationType
}
To use this particular initializer, you’ll have to pass in all the defaults yourself. To rectify that, we can make a different intializer that has defaults built in, by using the default parameter syntax. It’s getting really unwieldy, so let’s break it onto multiple lines as well.
init(title: String,
placeholder: String = "",
keyboardType: UIKeyboardType = .Default,
secureEntry: Bool = false,
autocorrectType: UITextAutocorrectionType = .None,
autocapitalizationType: UITextAutocapitalizationType = .None)
{
self.title = title
self.placeholder = placeholder
self.keyboardType = keyboardType
self.secureEntry = secureEntry
self.autocorrectType = autocorrectType
self.autocapitalizationType = autocapitalizationType
}
This initializer is mostly pretty good. Because of all of the default values, you can use this intializer with just a title, like so:
let fieldData = FieldData(title: "First Name")
You can also use it with any of the defaults overridden, like so:
let fieldData = FieldData(title: "First Name", secureEntry: true)
Even though secureEntry
is the fourth parameter, any parameters with defaults can be skipped. Swift does the right thing here, and that’s awesome. We could leave this as-is, but I wanted to go a step further. I don’t like how big the initializer is. Each property of the struct is declared in 3 places: first, in the property declaration; second, in the initializer’s function declaration, and lastly in the setter in the body of the declaration. This might not seem like a big deal, but every time you add, remove, or change a property, you’ll have to touch three pieces of code.
I toyed around with a few ways to fix this issue, including making the instance variables optional and filling in the defaults at the usage site, but that ended up being just as clunky as the big initializer. What I settled on was: instead of making the variables optional, I made them mutable. That solves quite a few problems. Let’s take a look:
struct FieldData {
let title: String
var placeholder = ""
var keyboardType = UIKeyboardType.Default
var secureEntry = false
var autocorrectType = UITextAutocorrectionType.No
var autocapitalizationType = UITextAutocapitalizationType.None
init(title: String) {
self.title = title
}
}
Our initializer is now dead simple. It has only the required parameters in it. Everything else has an easy to read default in the property declaration. We could add more spacing or documentation to those properties, as needed.
Next, I used a great little microlibrary called Then
, which helps clean up initialization of objects. It’s a dependency, but it has a really simple definition:
public protocol Then {}
extension Then {
public func then(@noescape block: inout Self -> Void) -> Self {
var copy = self
block(©)
return copy
}
}
That’s it. From this, we can extend our FieldData
struct with Then
:
extension FieldData: Then { }
And go to town:
let fieldData = FieldData(title: "Password").then({
$0.secureEntry = true
})
While this solution does have mutable properties, I think the wins in the readability of the call site and changeability of the code are worth it.
Using then
for view configuration
The then
extension is a really useful library to have in your app. By default, it extends all NSObject
types:
extension NSObject: Then { }
Since everything in Cocoa Touch inherits from NSObject
, you can now use this function to configure lots of types, and you can do so at the declaration of the property. Swift will let you initialize things in-line (as long as they’re effectively a one line expression):
let footerContainer = UIView().then({
$0.backgroundColor = UIColor.grayColor()
})
Since then
returns self
, you can also chain calls to then
. By moving common view configuration into a free function, like so:
struct Style
static func whiteButton(inout button: UIButton) {
button.setTitleColor(UIColor.whiteColor(), forState: .Normal)
button.contentVerticalAlignment = .Center
button.setBackgroundColor(UIColor.whiteColor(), forState: .Normal)
button.setBackgroundColor(UIColor.lightGrayColor, forState: .Hightlighted)
}
}
Because this function has the same form as the function that then
expects, you can pass it straight to then
, and call then
a second time to do more customized configuration:
let button = UIButton().then(Style.whiteButton).then({
$0.setTitle("Continue", forState: .Normal)
})
Go grab the then
function. It’s super useful.