DEV Community

Fernando Martín Ortiz
Fernando Martín Ortiz

Posted on

Expressiveness and extensions

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
}
Enter fullscreen mode Exit fullscreen mode

You could write an extension

extension Person {
    var greeting: String { 
        return "Hi, I'm \(name) and am \(age) years old" 
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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)),
]
Enter fullscreen mode Exit fullscreen mode

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)),
        ])
    }
}
Enter fullscreen mode Exit fullscreen mode

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)),
        ])
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)