Introduction
Swift is a beautiful and modern language. It's said to be expressive. As I understand expressiveness, it's the ability to express an idea using a medium. Swift code is expressive, because you can write code in Swift and once you get to it, you can express your ideas with minimal boilerplate in most situations.
There are many features in Swift that helps us in doing this. Today, we'll discuss a bit about extensions.
Considering we have a struct Person
:
struct Person {
let name: String
let age: Int
}
You could write an extension
extension Person {
var greeting: String {
return "Hi, I'm \(name) and am \(age) years old"
}
}
Pretty simple. Extensions come handy in many situations, but it's a good idea not to overuse them. Code too expressive is not expressive code. It becomes confusing.
Example: UIBarButtonItem
Suppose we're configuring some UIBarButtonItem
so we can add them to navigationItem
.
A good idea when you're trying to make this code expressive is by extending UIBarButtonItem
:
extension UIBarButtonItem {
static func done(target: Any?, action: Selector?) -> UIBarButtonItem {
return UIBarButtonItem(
image: UIImage(named: "Done"),
style: .done,
target: target,
action: action
)
}
static func favorite(target: Any?, action: Selector?) -> UIBarButtonItem {
return UIBarButtonItem(
image: UIImage(named: "Favorite"),
style: .done,
target: target,
action: action
)
}
}
Take this example. We're extending UIBarButtonItem
, so that when we need to configure buttons in the navigation bar, we can do this:
navigationItem.leftBarButtonItems = [
.done(target: self, action: #selector(doneButtonPressed)),
.favorite(target: self, action: #selector(doneButtonPressed)),
]
Example: Array
Now, this has worked and provides code that is more understandable and more expressive. But we can do better. Imagine we need to add more buttons to the navigation bar, in both sides of it, BUT only if the device is in landscape. A first attempt to do that would be:
func configureNavigationBar() {
navigationItem.leftBarButtonItems = [
.done(target: self, action: #selector(doneButtonPressed)),
.favorite(target: self, action: #selector(doneButtonPressed)),
]
if UIDevice.current.orientation == .landscapeLeft
|| UIDevice.current.orientation == .landscapeRight {
navigationItem.leftBarButtonItems?.append(contentsOf: [
.profile(target: self, action: #selector(doneButtonPressed)),
.posts(target: self, action: #selector(doneButtonPressed)),
])
}
}
This would work fine. If the device is in landscape, we append more items to the leftBarButtonItems
property.
We can make the orientation checking a bit cleaner by extending UIDevice
:
extension UIDevice {
var isLandscape: Bool {
return orientation == .landscapeLeft
|| orientation == .landscapeRight
}
}
//...
func configureNavigationBar() {
navigationItem.leftBarButtonItems = [
.done(target: self, action: #selector(doneButtonPressed)),
.favorite(target: self, action: #selector(doneButtonPressed)),
]
if UIDevice.current.isLandscape {
navigationItem.leftBarButtonItems?.append(contentsOf: [
.profile(target: self, action: #selector(doneButtonPressed)),
.posts(target: self, action: #selector(doneButtonPressed)),
])
}
}
However, there is an array extension that could make this much more cleaner:
extension Array {
func appending(_ element: @autoclosure () -> Element, if condition: Bool = true) -> [Element] {
if condition {
var copy = self
copy.append(element())
return copy
} else {
return self
}
}
}
There are some interesting things in this code fragment. That @autoclosure
thing means that the Element
will be put in memory and evaluated only if needed (when its auto-generated closure is executed). The condition
argument is true
, by default, meaning that it's completely optional.
This code will allow us to make the previous configureNavigationBar
function much cleaner. See:
func configureNavigationBar() {
navigationItem.leftBarButtonItems = []
.appending(.done(target: self, action: #selector(doneButtonPressed)))
.appending(.favorite(target: self, action: #selector(doneButtonPressed)))
.appending(.profile(target: self, action: #selector(doneButtonPressed)), if: UIDevice.current.isLandscape)
.appending(.posts(target: self, action: #selector(doneButtonPressed)), if: UIDevice.current.isLandscape)
}
The only problem that I see is that we're making unnecessary copies of the array. We're improving the code expressiveness at expenses of performance. Of course, in this example, the performance is minimal and I don't think it's something to consider, but anyway, it's good to keep that in mind.
The last improvement I'll add to it is this:
extension Array {
func appending(_ elements: @autoclosure () -> [Element], if condition: Bool = true) -> [Element] {
if condition {
var copy = self
copy.append(contentsOf: elements())
return copy
} else {
return self
}
}
}
If we put all the appending to the same condition, the result would be that the array will be cloned many less times:
func configureNavigationBar() {
navigationItem.leftBarButtonItems = [
.done(target: self, action: #selector(doneButtonPressed)),
.favorite(target: self, action: #selector(doneButtonPressed))
]
.appending([
.profile(target: self, action: #selector(doneButtonPressed)),
.posts(target: self, action: #selector(doneButtonPressed))
], if: UIDevice.current.isLandscape)
}
To sum up
We've seen a couple of examples where creating extensions could help us to create code that is more understandable at a first glance. It's ok and it's good to add extensions for this, but as always, this is a discipline of balancing tradeoffs. Too many extensions will result in a code that is not expressive at all, or a code that doesn't look like traditional Swift/iOS code. This is not something we want. We want our code to be understandable and minimize the time somebody will need to fully grasp it.
Top comments (0)