Community

Optimizing your Swift Codebase with Attributes

Editorial Note: Our community blog post series is authored by buddybuild users, and other respected mobile developers. This post is written by Jordan Morgan, iOS Developer at Buffer.

Swift + Attributes

I think all iOS developers have all been here: we just pick up programming, or start learning a new language where things are foreign - and we happen across some code. We may not understand it, but we think it works. So, we take it in good faith and continue on, none the wiser.

This process is exactly how I started incorporating Swift’s attributes into my codebase. Swift supports a robust variety of attributes, and when browsing Github repos - we may see one or two we don’t recognize. If you’re like me, you might jot it down to Google later, and then be on your way.

Why use Swift’s attributes?

Attributes help developers improve the efficiency of their codebase, without sacrificing the quality of their code. Their code is easier to read, easier to compile, and ultimately easier to maintain and safer to use.

Both in personal projects and at Buffer, efficiency is always at the forefront of my iOS development. This post enumerates some of Swift’s attributes that have improved my efficiency as a Swift developer. Let's dive in...

Groundwork

At the expense of starting by providing a boilerplate definition, I think it’s requisite to quickly hit on what exactly an attribute is. And really, all a Swift attribute is/does is provide more information about a type or declaration. This information can dictate everything from compiler warnings to how something is handled in memory.

No matter the flavour, each one is preceded by an “@” symbol. In addition, declaration attributes may also accept arguments inside of enclosing parentheses.

So, put simply:

@attributeName

//Or with arguments…
@attributeName(arguments)

Let’s look at some examples from the front lines.

Attributes

The @available(args) attribute

The Swiss Army Knife version of a Swift attribute is @available(). Flexible as it is powerful, you may find it indispensable if your daily duties revolve around managing or releasing an API. With it, one can indicate API naming changes, platform availability and more.

Consider an object from a metaphorical API that serves up blog posts:

class BasicPost {}  

Consumers of our API have long enjoyed using the BasicPost class, though imagine we’ve fielded several requests for a more honed in object that represents a technical blog post, much like the one you’re reading now. So for version 1.2,
we introduce it:

class TechnicalPost {}  

Now, to make our documentation complete, our code sensible and our API consumers informed we could take advantage of @available() to make its presence known:

@available(*, introduced: 1.2)
class TechnicalPost {}  

This particular attribute can accept several arguments, but the first one always indicates the intended platforms. The remaining arguments supported can be supplied in any order.

It can also take advantage of a wildcard. In this case, the wildcard is the asterisk you’re seeing as the first argument — which communicates that on all platforms the API is used on, this class was first introduced on version 1.2 (represented by the second argument).

That’s neat, but also extremely broad. Thankfully, we can focus it in even more with a shorthand syntax:

@available(iOS 10.0, macOS 10.12)
class TechnicalPost {}  

Much better! Now, our fictional API can clearly see when this class can be used even if it doesn't know much about the attribute itself.

But, if left as is, you’ll also receive a compile time error. Why?

Apple has a tendency to introduce new platforms, and we need to enforce our code to account for this. To do so, we include the wildcard as the last argument to signify that this code is available for the provided platforms, and any potential future platforms:

@available(iOS 10.0, macOS 10.12, *)
class TechnicalPost {}  

This way, your code is already set up for the next big thing Apple's planning. For now though, Apple has provided us with an enumeration representing each platform as they exist today:

  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension

Before we move to the next attribute, let’s consider another commonality. With our recent changes, our API has taken off, and iOS developers around the world have entrusted us with serving them up with lovely bits of JSON that represent technical blog posts.

As such, we no longer have a need for the original class anymore. And, it's time to deprecate it:

@available(*, deprecated: 1.3)
class BasicPost {}  

Now it becomes clear how useful the wildcard argument can be, as in one (ahem) swift move we’ve deprecated BasicPost on all platforms.

Further, if we wanted to keep it around but a bit refactored, we can even provide notice of an API naming change. Courtesy of a technique I caught from Apple, we can pair it with an unavailable argument and a typealias to make things even easier for consumers:

//From an earlier API version
class BasicPost {}

//From a new API version, where we renamed it for whatever reason
class BaseTechnicalPost {}

@available(*, unavailable, renamed: “BaseTechnicalPost”)
typealias BasicPost = BaseTechnicalPost  

I personally love this, because for my money and from an efficiency perspective — the most obvious code is always the best code.

This attribute has even more tricks, with support for arguments specifying some code obsolete, a message to provide in conjunction with a warning or error and
more.

The @discardableResult attribute

If you’ve worked in a mature, legacy codebase it should come as no surprise that some functions can be a bit verbose.

Perhaps you've come across an existing function that's been added to and manipulated since the early 90s, and due to no fault of its own — it might do 124 important things that need to happen when the software starts (access a
database, setup a cache, initialize some sign in process — the scenarios are endless).

Until the day you convince the product manager to let you come back and do some necessary refactoring.

Right. Right!? Right:

let someUnusedVarBecauseIHaveToCallThisOldInsaneFunction = anOldInsaneFunction()  

However, clang will complicate things further because even though we’ve got to invoke this function for some outlandish coupling reason — we now have an unused variable to show for it as well. Talk about getting kicked while you’re down...

This is where the @discardableResult attribute can help, as it tells the compiler that the result of the function may be unneeded. It also kills the warning at compile time:

@discardableResult func anOldInsaneFunction() -> String
{
    //Bunch of business logic occurs
    return “”
}

Now, the code above which invokes the said function will stay there only as a relic of your past software engineering mistakes — but it will do so without providing an error.

For added clarity, one could make the state of affairs even more obvious by simply assigning to a _:

_ = anOldInsaneFunction()  

Sometimes, there are functions or architecture in software development we can’t directly control or fix, and this attribute makes that situation a little bit
better.

The @autoclosure attribute

The @autoclosure is another handy attribute that adds clarity to your codebase. It automatically wraps a closure that’s supplied as an argument. Since the closure doesn’t take any arguments itself, it’ll return the actual value of the expression that’s wrapped within it.

This may seem confusing, but it’s easily understood when you come across one. At a high level, we're talking about the ability to get an expression to automatically become a closure. If you’ve spent some time adding unit tests to
your project, you’ve likely come across this attribute several times already.

Assume we’d like to write a simple test for a class like so:

class Programmer  
{
    var pay:Int

    init(withPay pay:Int)
    {
        self.pay = pay
    }

    func applyRaise(by amount:Int)
    {
        self.pay += amount
    }
}

class ProgrammerTests: XCTestCase  
{
    func testPayRaise()
    {
        let devsPay = 50000
        let raiseAmount = 25000
        let expectedSalaryPostRaise = devsPay + raiseAmount

        let aDev = Programmer(withPay: devsPay)
        aDev.applyRaise(by: raiseAmount)

        XCTAssertEqual(expectedSalaryPostRaise, aDev.pay, "Unexpected salary after raise was applied.")
     }
}

The first two parameters of the XCAssertEqual are both closures that take in a generic expression. While the function’s signature can look a little intimidating, take note of the first two parameters that are taking advantage of @autoclosure.

func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> T?, _ expression2: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) where T : Equatable  

Since the @autoclosure attribute is supplied, invoking the function is quite readable and easier on us. We can either pass the closure with something as simple as a value (as we did in our previous example) or with a bit more logic, and each one is trivial to use:

class ProgrammerTests: XCTestCase  
{
    func testPayRaise()
    {
        let devsPay = 50000
        let raiseAmount = 25000

        let aDev = Programmer(withPay: devsPay)
        aDev.applyRaise(by: raiseAmount)

        XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
    }
}

Take note that when the first argument is supplied, it reads much more like an addition operation than it does a closure:

XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")  

versus what it might look like without the @autoclosure attribute

XCTAssertEqual({  
    return aDev.pay + raiseAmount,
}, {
    return 75000
}, "Unexpected salary after raise was applied.")

As you can see, passing a fully qualified closure (in terms of syntax) — it’s a bit much to read and write. That would be a compounded problem if one can’t use a trailing closure as the last argument.

So with @autoclosure, that’s essentially what we mean when we say that the closure returns the actual value that’s wrapped inside of it. You might even say the parameter became a closure…automatically, thus, @autoclosure!

This code is also inherently delayed. This is an added benefit if the actual closure might end up being an expensive task or it might bring about some unintended side effects. The code provided is never executed until the closure it’s wrapped in is.

Going further, where else might you have seen this in your recent iOS endeavours? How about assert()?

struct Programmer  
{
    var isSenior:Bool
    var appsShipped:Int
}

let aSeniorDev = Programmer(isSenior: true, appsShipped: 13)  
assert(aSeniorDev.isSenior, “This dev isn’t a senior!”)  

The first argument provided uses @autoclosure, if it weren’t we’d invoke it closer to something like this:

assert({  
    return aSeniorDev.isSenior 
}, { 
    return “This dev isn’t a senior!”
})

With @autoclosure, the code is a bit easier on us to write, and I would also argue that it also makes for a far more enjoyable reading experience.

And, if you’re curious how assert()’s signature looks, it’s something like this:

func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)  

If you’re thrown off by the other two parameters in the signature, we omitted them in the example because they have default values assigned to them. The more you know, right?

Using Multiple Attributes

While each attribute has benefits that help it stand on their own, it’s also helpful to pair them together in certain scenarios.

For example, take the @escaping attribute. The @escaping attribute signifies that the passed in closure can outlast the function it’s passed to.

//A property on a view controller
var onFakeCompletions:[()->()] = []

func fakeNetworkOp(_ completion:@escaping ()->())  
{
    //Network stuff happens

    //The closure is appended to an external array outside of the function's scope. This implies it could be invoked outside of the function - i.e., it could "escape" it
    onFakeCompletions.append(completion)
}

Considering this, we could pair both @escaping with @autoclosure for the same parameter. As an example, let's imagine H.R. let us know that any developer who is both a "Senior" in title and has shipped at least three apps is due for a raise, but we also need to keep track of each evaluation for historical purposes:

class Programmer  
{
    var previousPayRaiseEvaluations:[()->Bool] = []
    var isSenior:Bool = false
    var appsShipped:Int = 0

    func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
    {
        if raiseEvaluation()
        {
            //Give them a raise, and then save it to their records
            previousPayRaiseEvaluations.append(raiseEvaluation)
        }
    }
}

let aProgrammer = Programmer()  
aProgrammer.isSenior = true  
aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1


    func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
    {
        if raiseEvaluation()
        {
            //Give them a raise, and then save it to their records
            previousPayRaiseEvaluations.append(raiseEvaluation)
        }
    }
}

let aProgrammer = Programmer()  
aProgrammer.isSenior = true  
aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1  

There’s certainly nothing prohibiting you from “chaining” attributes together, and when the situation calls for it, it works rather seamlessly.

Final Thoughts

Attributes have always been something I’ve been particularly passionate about using in my code. I like the idea of doing some powerful heavy lifting in a clear, concise and simple way — and that’s really what attributes do for you. Of course, there are the heavy hitters worth knowing, such as the ubiquitous @objc attribute in Swift + Objective-C projects.

Considering that, an argument could be made that attributes are more akin to a necessity rather than a nicety. In the end, it’s all about optimizing your workflow as a developer, be it in your codebase or otherwise. Attributes are just one way to achieve this.

Once your application is complete, services like buddybuild can further improve your efficiency by doing the heavy lifting to automate your continuous integration and deployment processes.

At Buffer, we're always looking for ways to optimize our development workflow, and these are just some of the ways we’ve discovered to help us do so.

Subscribe to receive other buddybuild updates.