Swift 5.3 brings with it another raft of improvements for Swift, including some powerful new features such as multi-pattern catch clauses and multiple trailing closures, plus some important changes for Swift Package Manager.

In this article I’m going to walk through each of the major changes, while providing hands-on code samples so you can try them yourself. I encourage you to follow the links through to the Swift Evolution proposals for more information, and if you missed my earlier what’s new in Swift 5.2 article then check that out too.

Multi-pattern catch clauses

SE-0276 introduced the ability to catch multiple error cases inside a single catch block, which allows us to remove some duplication in our error handling.

For example, we might have some code that defines two enum cases for an error:

enum TemperatureError: Error {
    case tooCold, tooHot
}

When reading the temperature of something, we can either throw one of those errors, or send back “OK”:

func getReactorTemperature() -> Int {
    90
}

func checkReactorOperational() throws -> String {
    let temp = getReactorTemperature()

    if temp < 10 {
        throw TemperatureError.tooCold
    } else if temp > 90 {
        throw TemperatureError.tooHot
    } else {
        return "OK"
    }
}

When it comes to catching errors thrown there, SE-0276 lets us handle both tooHot and tooCold in the same way by separating them with a comma:

do {
    let result = try checkReactorOperational()
    print("Result: \(result)")
} catch TemperatureError.tooHot, TemperatureError.tooCold {
    print("Shut down the reactor!")
} catch {
    print("An unknown error occurred.")
}

You can handle as many error cases as you want, and you can even bind values from your errors if needed.

Multiple trailing closures

SE-0279 introduced multiple trailing closures, making for a simpler way to call a function with several closures.

This will be particularly welcome in SwiftUI, where code like this:

struct OldContentView: View {
    @State private var showOptions = false

    var body: some View {
        Button(action: {
            self.showOptions.toggle()
        }) {
            Image(systemName: "gear")
        }
    }
}

Can now be written as this:

struct NewContentView: View {
    @State private var showOptions = false

    var body: some View {
        Button {
            self.showOptions.toggle()
        } label: {
            Image(systemName: "gear")
        }
    }
}

Technically there is no reason why label: needs to be on the same line as the preceding }, so you could even write this if you wanted:

struct BadContentView: View {
    @State private var showOptions = false

    var body: some View {
        Button {
            self.showOptions.toggle()
        }

        label: {
            Image(systemName: "gear")
        }
    }
}

However, I would caution against that for readability – a floating piece of code like that is never pleasant, and in Swift it looks like a labeled block rather than a second parameter to the Button initializer.

Note: There was quite a lot of heated discussion about multiple trailing closures on the Swift forums, and I would like to use this opportunity to remind folks to be civil when taking part in our community. Notable syntax changes like this one are always strange at first, but please give it time and see how you get on in practice.

Synthesized Comparable conformance for enums

SE-0266 lets us opt in to Comparable conformance for enums that either have no associated values or have associated values that are themselves Comparable. This allows us to compare two cases from the same enum using <>, and similar.

For example, if we had an enum that describes clothing sizes we could ask Swift to synthesize Comparable conformance like this:

enum Size: Comparable {
    case small
    case medium
    case large
    case extraLarge
}

We can now create two instances of that enum and compare them using <, like this:

let shirtSize = Size.small
let personSize = Size.large

if shirtSize < personSize {
    print("That shirt is too small")
}

This synthesized conformance works great with associated values that are Comparable. For example, if we had an enum that described the football World Cup wins for a team, we might write this:

enum WorldCupResult: Comparable {
    case neverWon
    case winner(stars: Int)
}

We could then create several instances of that enum with varying values, and have Swift sort them:

let americanMen = WorldCupResult.neverWon
let americanWomen = WorldCupResult.winner(stars: 4)
let japaneseMen = WorldCupResult.neverWon
let japaneseWomen = WorldCupResult.winner(stars: 1)

let teams = [americanMen, americanWomen, japaneseMen, japaneseWomen]
let sortedByWins = teams.sorted()
print(sortedByWins)

That will sort the array so that the two teams who haven’t won the World Cup come first, then the Japanese women’s team, then the American women’s team – it considers the two winner cases to be higher than the two neverWon cases, and considers winner(stars: 4) to be higher than winner(stars: 1).

self is no longer required in many places

SE-0269 allows us to stop using self in many places where it isn’t necessary. Prior to this change, we’d need to write self. in any closure that referenced self so we were making our capture semantics explicit, however often it was the case that our closure could not result in a reference cycle, meaning that the self was just clutter.

For example, before this change we would write code like this:

struct OldContentView: View {
    var body: some View {
        List(1..<5) { number in
            self.cell(for: number)
        }
    }

    func cell(for number: Int) -> some View {
        Text("Cell \(number)")
    }
}

That call to self.cell(for:) cannot cause a reference cycle, because it’s being used inside a struct. Thanks to SE-0269, we can now write the same code like this:

struct NewContentView: View {
    var body: some View {
        List(1..<5) { number in
            cell(for: number)
        }
    }

    func cell(for number: Int) -> some View {
        Text("Cell \(number)")
    }
}

This is likely to be extremely popular in any framework that makes heavy use of closures, including SwiftUI and Combine.

Type-Based Program Entry Points

SE-0281 introduces a new @main attribute to allow us to declare where the entry point for a program is. This allows us to control exactly which part of our code should start running, which is particularly useful for command-line programs.

For example, when creating a terminal app previously we needed to create a file called main.swift that was able to bootstrap our code:

struct OldApp {
    func run() {
        print("Running!")
    }
}

let app = OldApp()
app.run()

Swift automatically considered code in main.swift to be top-level code, so it would create the App instance and run it. That is still the case even after SE-0281, but now if you want to you can remove main.swift and instead use the @main attribute to mark a struct or base class that contains a static main() method to be used as the program’s entry point:

@main
struct NewApp {
    static func main() {
        print("Running!")
    }
}

When that runs, Swift will automatically call NewApp.main() to start your code.

The new @main attribute will be familiar to UIKit and AppKit developers, where we use @UIApplicationMain and @NSApplicationMain to mark our app delegates.

However, there are some provisos you should be aware of when using @main:

  • You may not use this attribute in an app that already has a main.swift file.
  • You may not have more than one @main attribute
  • The @main attribute can be applied only to a base class – it will not be inherited by any subclasses.

where clauses on contextually generic declarations

SE-0267 introduced the ability to attach a where clause to functions inside generic types and extensions.

For example, we could start with a simple Stack struct that let us push and pop values from a private array:

struct Stack<Element> {
    private var array = [Element]()

    mutating func push(_ obj: Element) {
        array.append(obj)
    }

    mutating func pop(_ obj: Element) -> Element? {
        array.popLast()
    }
}

Using SE-0267, we could add a new sorted() method to that stack, but only for times when the elements inside the stack conform to Comparable:

extension Stack {
    func sorted() -> [Element] where Element: Comparable {
        array.sorted()
    }
}

New collection methods on noncontiguous elements

SE-0270 introduces a new RangeSet type that can represent any number of positions in a collection. This might sound fairly simple, but it actually enables some highly optimized functionality that will prove useful in many programs.

For example, if we had an array of names like this:

var names = [
    "Eleanor",
    "Chidi",
    "Tahani",
    "Jianyu",
    "Michael",
    "Janet"
]

We could get a RangeSet containing the locations of all names that being with J:

let jNames = names.subranges(where: { $0.hasPrefix("J") })

That won’t return the names themselves, only their locations in the names array.

As the name implies, RangeSet implements a subset of the SetAlgebra protocol, allowing us to create unions, intersections, subsets, and more, while also checking whether a RangeSet contains specific items.

So, we could create a second range set then combine the two together like this:

let mNames = names.subranges(where: { $0.hasPrefix("M") })
let combinedNames = jNames.union(mNames)

You can also use contains(), but note that has a complexity of O(log n) compared to the O(1) from a regular Set.

The new RangeSet type has been fully integrated into the rest of the standard library, allowing us to use it for subscripting collections. For example, if we had an array of numbers like this:

 let numbers = [-4, 8, -15, 16, -23, 42]

Then we could pull out all the positive numbers, sum them, the print information about how many we found and their sum:

 let positiveSubranges = numbers.subranges { $0 > 0 }
 let positiveSum = numbers[positiveSubranges].reduce(0, +)
 print("There are \(numbers[positiveSubranges].count) positive numbers, totaling \(positiveSum)")

Perhaps most usefully of all, this change gives us a new moveSubranges(_:to:) method for moving all items in a RangeSet to a particular point in a mutable collection, while preserving the order of the items you’re moving. So, this will move all our positive numbers to the end of the array:

numbers.moveSubranges(positiveSubranges, to: numbers.endIndex)

This method will return the new indices of the objects, so you can see where they were individually moved to.

Refined didSet Semantics

SE-0268 adjusts the way the didSet property observers work so that they are more efficient. This doesn’t require a code change unless you were somehow relying on the previous buggy behavior; you’ll just get a small performance improvement for free.

Internally, this change makes Swift not retrieve the previous value when setting a new value in any instance where you weren’t using the old value, and if you don’t reference oldValue and don’t have a willSet Swift will change your data in-place.

If you do happen to be relying on the old behavior, you can work around it simply by referencing oldValue to trigger your custom getter, like this:

didSet {
    _ = oldValue
}

A new Float16 type

SE-0277 introduced a new half-precision floating point type called Float16, which is commonly used in graphics programming and machine learning.

This new floating-point type fits in alongside Swift’s other similar types:

let first: Float16 = 5
let second: Float32 = 11
let third: Float64 = 7
let fourth: Float80 = 13

Swift Package Manager gains binary dependencies, resources, and more

Swift 5.3 introduced many improvements for Swift Package Manager (SPM). Although it’s not possible to give hands-on examples of these here, we can at least discuss what has changed and why.

First, SE-0271 (Package Manager Resources) allows SPM to contain resources such as images, audio, JSON, and more. This is more than just copying files into a finished app bundle – for example, we can apply a custom processing step to our assets, such as optimizing images for iOS. This also adds a new Bundle.module property for accessing these assets at runtime. SE-0278 (Package Manager Localized Resources) builds on this to allow for localized versions of resources, for example images that are in French.

Second, SE-0272 (Package Manager Binary Dependencies) allows SPM to use binary packages alongside its existing support for source packages. This means common closed-source SDKs such as Firebase can now be integrated using SPM.

Third, SE-0273 (Package Manager Conditional Target Dependencies) allows us to configure targets to have dependencies only for specific platforms and configurations. For example, we might say that we need some specific extra frameworks when compiling for Linux, or that we should build in some debug code when compiling for local testing.

It’s worth adding that the “Future Directions” section of SE-0271 mentions the possibility of type-safe access to individual resource files – the ability for SPM to generate specific declarations for our resource files as Swift code, meaning that things like Image("avatar") become something like Image(module.avatar).