What's new in Swift 5.2

2020-06-11

What's new in Swift 5.2
  1. Introduction
  2. Keypath expressions as functions
  3. Callable values of nominal types
  4. Subscripts with default arguments
  5. Lazy filtering order has changed
  6. New diagnoses
  7. Conclusions
  8. References

Introduction

Swift 5.2 arrived with Xcode 11.4 and includes a handful of language changes along with reductions in code size and memory usage, plus a new diagnostics architecture that will help you understand and resolve errors faster. In this article, I'll break down what's new in Swift 5.2 with some practical examples so you can see for yourself how things have evolved. I encourage you to follow the links to the Swift Evolution proposals to learn more.

Key path expressions as functions

SE-0249 introduced a wonderful shortcut that allows us to use keyboard paths in a handful of specific circumstances.

Evolution's proposal describes this as being able to use "\Root.value where Root -> Value is allowed", but what it means is that you can retrieve a property of an object using it as a key path: Car.licensePlate

This is best understood as an example, so here is a User type that defines four properties:

struct User {
    let name: String
    let age: Int
    let bestFriend: String?

    var canVote: Bool {
        age >= 18
    }
}

We could create some instance of that structure and put it in an array, like this:

let eric = User(name: "Eric Effiong", age: 18, bestFriend: "Otis Milburn")
let maeve = User(name: "Maeve Wiley", age: 19, bestFriend: nil)
let otis = User(name: "Otis Milburn", age: 17, bestFriend: "Eric Effiong")
let users = [eric, maeve, otis]

Now for the important part: if you want to get an array of all the usernames, you can do it using a key path like this:

let userNames = users.map(\.name)
print(userNames)

Previously, you would have had to write a closure to retrieve the name by hand, like this:

let oldUserNames = users.map { $0.name }

This same approach works elsewhere: anywhere you would have previously received a value and passed one of its properties, you can now use a key path. For example, this will return all users who can vote:

let voters = users.filter(\.canVote)

And this will return best friends for all users who have one:

let bestFriends = users.compactMap(\.bestFriend)

Callable values of user-defined nominal types

SE-0253 introduces statically callable values to Swift, which is a fancy way of saying that you can now call a value directly if your type implements a method called callAsFunction(). You don't need to conform to any special protocols for this behavior to work; you just need to add that method to your type.

For example, we could create astructure that has properties for lowerBound and upperBound, and then add callAsFunction so that every time you call a value of Dice you get a random roll:

struct Dice {
    var lowerBound: Int
    var upperBound: Int

    func callAsFunction() -> Int {
        (lowerBound...upperBound).randomElement()!
    }
}

let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6()
print(roll1)

That will print a random number from 1 to 6, and is identical to just using callAsFunction() directly. For example, we could call it like this:

let d12 = Dice(lowerBound: 1, upperBound: 12)
let roll2 = d12.callAsFunction()
print(roll2)

Swift automatically adapts its call sites based on how callAsFunction() is defined. For example, you can add as many parameters as you want, you can control the return value, and you can even mark methods as mutating if necessary.

For example, a StepCounter structure that tracks how far someone has walked and reports whether they reached their goal of 10,000 steps:

struct StepCounter {
    var steps = 0

    mutating func callAsFunction(count: Int) -> Bool {
        steps += count
        print(steps)
        return steps > 10_000
    }
}

var steps = StepCounter()
let targetReached = steps(count: 10)

For more advanced use, callAsFunction()*supports both throwsand rethrows, and you can even define multiple *callAsFunction()*methods in a single type: Swift will choose the correct one based on the parameters of the call, just like regular overloading.

Subscripts can now declare default arguments

When adding custom subscripts to a type, you can now use default arguments for any of the parameters. For example, if we had a PoliceForce structure with a custom subscript to read police officers, we could add a defaultparameter to send back if someone tries to read an index outside the bounds of the array:

struct PoliceForce {
    var officers: [String]

    subscript(index: Int, default default: String = "Unknown") -> String {
        if index >= 0 && index < officers.count {
            return officers[index]
        } else {
            return `default`
        }
    }
}

let force = PoliceForce(officers: ["Amy", "Jake", "Rosa", "Terry"])
print(force[0])
print(force[5])

This will print "Amy" and then "Uknown", and the latter is because there is no official one at index 5. Note that you must type its parameter tags twice if you want them to be used, because subscripts don't use parameter tags otherwise.

So, since I use default default in my subscript, I can use a custom value like this:

print(force[-1, default: "The Vulture"])

Lazy filtering order is now reversed

There's a small change in Swift 5.2 that could cause its functionality to break: if you use a lazy stream as an array and apply multiple filters to it, those filters are now executed in reverse order.

For example, this code below has one filter that selects names that start with S, then a second filter that prints the name and then returns true:

let people = ["Arya", "Cersei", "Samwell", "Stannis"]
    .lazy
    .filter { $0.hasPrefix("S") }
    .filter { print($0); return true }
_ = people.count

In Swift 5.2 and later, "Samwell" and "Stannis" will be printed, because after the first filter is run, those are the only names left to go into the second filter. But before Swift 5.2, it would have returned all four names, because the second filter would have run before the first. This was confusing, because if you removed the lazycode, the code would always return only Samwell and Stannis, regardless of the Swift version.

This is particularly problematic because its behavior depends on where the code is running: if you run Swift 5.2 code on iOS 13.3 or earlier, or macOS 10.15.3 or earlier, you'll get the old backwards behavior, but the same code running on newer operating systems will give the new correct behavior.

So this is a change that may cause surprise breaks in your code, but hopefully it's just a short-term inconvenience.

New and improved diagnostics

Swift 5.2 introduced a new diagnostic architecture that aims to improve the quality and accuracy of the error messages issued by Xcode when you make a code error. This is particularly evident when working with SwiftUI code, where Swift would often produce false positive error messages.

For example, code like this:

struct ContentView: View {
    @State private var name = 0

    var body: some View {
        VStack {
            Text("What is your name?")
            TextField("Name", text: $name)
                .frame(maxWidth: 300)
        }
    }
}

That attempts to bind a TextFieldview to a @Stateproperty of type Int, which is invalid. In Swift 5.1, this caused an error for the frame() modifier that said 'Int' is not convertible to 'CGFloat?', but in Swift 5.2 and later, this correctly identifies the error as the $namebinding: Cannot convert value of type Binding<Int> to the expected argument type Binding <String> .

More information about the new diagnostic architecture can be found on the Swift.org blog.

Conclusions

The Swift project has reached a critical milestone of core fundamentals maturity, providing stability for users to invest in using Swift seriously. On Apple platforms such as macOS and iOS, the arrival of ABI and module stability has allowed the creation of stable binary frameworks. Additionally, Swift Package Manager, which has built-in support in both Xcode and other IDEs, provides a cross-platform solution for building and distributing Swift libraries. Together, these form the critical ingredients to foster the development of a thriving Swift software ecosystem.

References

  1. Swift.org blog
  2. Apple Swift repo
  3. New Diagnostic Architecture
  4. Swift 5.2 Release changes