Articles, podcasts and news about Swift development, by John Sundell.

When can Swift’s return keyword be omitted?

Answered on 21 Mar 2021

Ever since the very first version of Swift, we’ve been able to omit the return keyword within single-expression closures, such as this one, which attempts to convert each element within an array of String values into an equivalent Int:

let strings = ["1", "2", "3"]

let ints = strings.compactMap { string in
    Int(string)
}

However, if a given closure contains multiple expressions, then the return keyword can’t be omitted, even if that closure doesn’t include any conditions or separate code branches. So the closure passed into the below call to map needs to explicitly mark its last expression as its return value:

class GameController {
    private(set) var players = [Player]()

    func reviveAllPlayers() {
        players = players.map { player in
            var player = player
            player.isActive = true
            player.hitPoints = 100
            return player
        }
    }
}

To learn more about the map and compactMap functions used above, check out this Basics article.

In Swift 5.1, the above behavior was expanded to also include functions and computed properties, while maintaining the exact same rules. So now, when writing a function that contains just a single expression that computes a return value, then we can also omit the return keyword — like this:

extension GameController {
    func playersQualifiedForNextLevel() -> [Player] {
        players.filter { player in
            player.isActive && player.score > 1000
        }
    }
}

Above we’re actually omitting two return keywords — both the top-level one within our function, and the one for the closure passed to filter.

Likewise, when implementing a computed property that simply returns the outcome of a single expression, then the return keyword can now also be omitted — which often makes it possible to fit an entire computed property implementation on a single line of code, if that’s something that we’d like to do:

extension GameController {
    var gameCanBeStarted: Bool { players.count > 1 }
}

Worth noting, though, is that all of these features are entirely optional. If we wanted to, we could modify each of the examples that we’ve taken a look at so far to instead always use explicit return keywords, and everything would keep working the exact same way.

One thing that could potentially make the above behaviors slightly more confusing, however, is when we start to adopt SwiftUI. When using a closure to construct the body of a SwiftUI container, it might initially seem like we’re actually able to omit the return keyword even within closures that contain multiple expressions or code paths — like this:

struct RootView: View {
    @ObservedObject var loginController: LoginController

    var body: some View {
        NavigationView {
            if let user = loginController.loggedInUser {
                HomeView(user: user)
            } else {
                LoginView(handler: loginController.performLogin)
            }
        }
    }
}

However, the above closure is not actually using multiple implicit returns, but is instead being processed by SwiftUI’s ViewBuilder — which is a function/result builder that takes each of our view expressions and combines them into a single return type.

To illustrate, let’s see what happens if we turn the above closure into a method instead:

struct RootView: View {
    @ObservedObject var loginController: LoginController

    var body: some View {
        NavigationView(content: makeContent)
    }

    private func makeContent() -> some View {
    if let user = loginController.loggedInUser {
        HomeView(user: user)
    } else {
        LoginView(handler: loginController.performLogin)
    }
}
}

When attempting to compile the above code, we’ll now get the following build error:

Function declares an opaque return type, but has no return
statements in its body from which to infer an underlying type.

To fix it, we’d have to mark our new makeContent method with the same @ViewBuilder attribute that SwiftUI marks many of its closure parameters with — which once again makes it possible for us to declare multiple expressions without any return keywords:

struct RootView: View {
    @ObservedObject var loginController: LoginController

    var body: some View {
        NavigationView(content: makeContent)
    }

    @ViewBuilder private func makeContent() -> some View {
        if let user = loginController.loggedInUser {
            HomeView(user: user)
        } else {
            LoginView(handler: loginController.performLogin)
        }
    }
}

So, to sum things up: