The Ins and Outs Of Swift Closures

If you’re just getting into Swift development, closures can be a scary topic. It’s easy to see complex return types and shut your brain off entirely. But properly understanding them is pretty much essential to be a successful iOS developer these days.  Especially with the rapid adoption of SwiftUI, which utilizes closures extensively.

So slow down, take a deep breath, and get ready. This post is going to walk through the absolute fundamentals of how closures work and build upon that one step at a time. And by the time you reach the bottom you’ll be able to use them in complex situations. They’re tricky at first, but once you get a sound grasp on the building blocks they click and are actually very straightforward. Ready? Let’s go:

  1. Defining a Closure:

Before doing anything fancy with them, we have to both know what a closure is and how to create one. In swift, a closure is a function that can be assigned to and accessed from a variable.  What this means is that we can create functions in swift (just like any other language), and represent that entire function via a variable the same way we do for String or Int. Like so:

let closure = {
print("closure activated")
}

This creates a function that will print “closure activated” when we call it, and it’s stored in the variable closure. We can call it by adding parentheses to the end of the variable just as we would call any other function.

closure()

Clojure GIFs - Get the best GIF on GIPHY

Boom, you just wrote and called your first closure (they get more complex don’t worry).

  1. Using Your Closure as a Variable

Now that we’ve created our closure, let’s demonstrate how it can be used. Since we can pass around this closure like a variable let’s create another function that takes in a closure as a single argument.

func functionAcceptingClosure(action: () -> Void) {
    print("function started")
    action()
    print("function ended")
}

In the above code we declare a new function that takes in one argument which we have named action, and the type for that argument is () -> Void.  Here’s where the brain starts flipping a switch to shut off, but take a second to think about what this means.  () -> Void is our syntax to represent a function that takes in 0 parameters and has no return type either. action’s type is a closure matching these argument requirements. And we just so happen to have already defined a closure that takes in 0 parameters and has no return type. Let’s  combine these two:

functionAcceptingClosure(action: closure)

If you ran this code in a new Swift playground your output would be:

function started
closure activated
function ended

We call our functionAcceptingClosure method and pass in our closure variable as the one parameter it’s expecting. Then the function goes through it’s regular logic and when that logic involves calling the action…well we’ve defined exactly what that action is.

  1. Trailing Closure Syntax:

For our convenience Swift also offers something known as trailing closure syntax.  If the last parameter in a function is a closure, then we can move it outside of the parentheses and into a set of brackets like so:

functionAcceptingClosure {
closure()
}

While in this specific scenario there’s not a huge readability benefit to doing so, there are other situations it is.  Trailing closure syntax is used all over the Swift world, so it’s best to get comfortable with it now.  Also, at WWDC this year Apple announced the introduction of multiple trailing closures, but we won’t get into those in this post.

  1. Accepting and Returning Values in a Closure:

Since closures are functions, they can both accept arguments and have a return type.  Let’s define a closure that takes in one String as an argument:

let closureWithArgs = { (value: String) in
    print("arg is \(value)")
}

closureWithArgs(“argument”)

This is similar to our first closure definition, but now we include (value: String) in in our definition.  value is the String variable this closure will requires any time it is called, and the in keyword tells us where the arguments end and the closure’s logic begins.

Now if we want our closure to also return a value we use the same -> syntax functions usually use. The key difference being this return type is included along with the arguments before the in keyword. Here’s an example of a closure that takes in an Int and returns one:

let closureReturningValue = { (value: Int) -> Int in
    let newValue = value * 2
    return newValue
}

If we called this closure (remember closures are just special functions, so they can be called by themselves) by typing closureReturningValue(4) we would get a return value of 8. We can assign this full closure implementation to an Int variable and print it like so:

let returnedValue = closureReturningValue(4)
print(returnedValue)  //This prints 8

  1. Getting Into Complex Cases:

Let’s pause for a moment and recap everything we jut created.  A closure is a special function that can be references via a variable the same way we would a String or Int.  Closures can take in any number of arguments, and they can return values the same way other functions would. And because closures are referenced as variables, we can pass them in to other functions as an argument. Potentially we could chain dozens of closures that each take another closure as one of their parameters.

That’s really it for establishing the building blocks.  My recommendation is the make simple closures like these again and again. And when you feel comfortable try adding a few more arguments or expanding the ways they are referenced.  For example, now let’s try building a more complex case: Let’s say we need to create a function that takes in both an Int and a closure, and it needs that closure to perform some action on the Int and return a new one to be printed (aka the closure needs to take in an Int and return an Int). Why would we do this? We’ll go over that in a second, but first let’s build this function/closure:

func functionAcceptingClosureThatReturnsValue(startingValue: Int, action: (Int) -> Int) {
    let returnedValue = action(startingValue)
    print(returnedValue)

}

In this function, startingValue is the Int we are passing in and action is the closure that will take in an int and then return a new one. Note that this function does nothing to specify what action does. Aha! That’s the whole point of closures.  Because this function doesn’t know what the action does, we can use it in a number of different scenarios each where action does something else.  Maybe it doubles the Int, maybe it adds 3, that doesn’t matter until we decide what the action will be.

Let me restate this as it’s what this whole post has been building up to:

Because this function doesn’t know what the closure argument does, we can call it multiple times and pass in different closures depending on our use case each time.

Let’s say at one point in our app we want to call this function and the closure needs to double the startingValue.  We can call this like so:

functionAcceptingClosureThatReturnsValue(startingValue: 5, action: closureReturningValue)

When this line of code is called we pass in our closureReturningValue closure which will take in 5 and return 10.  Then the print(returnedValue) part of the function prints 10.  If we wanted to instead triple the number, we would just need to write a new closure that does so, and then pass that in as the action.

Closing Up:

Closures are incredibly powerful and can lead to some really sleek code if done in the right way.  They’re a little tricky to wrap your head around at first, but something clicks after enough poking around, and then you can’t imagine living without them.  They’re also used all over the place in SwiftUI, so if you’re hoping to continue developing iOS apps over the next few years it will be impossible to do so without thoroughly understanding how they work and the benefits they bring.

 

 

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s