UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

What’s new in Swift 5.3?

Multiple trailing closures, massive package manager improvements, and more.

Paul Hudson       @twostraws

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.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

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() -> 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()
    }
}

Enum cases as protocol witnesses

SE-0280 allows enums to participate in protocol witness matching, which is a technical way of saying they can now match requirements of protocols more easily.

For example, you could write code to handle various types of data, but what if that data were missing? Sure, you could use something like nil coalescing to provide a default value every time, but you could also make a protocol that requires a default value, then make various types conform to it with whatever default values you wanted:

protocol Defaultable {
    static var defaultValue: Self { get }
}

// make integers have a default value of 0
extension Int: Defaultable {
    static var defaultValue: Int { 0 }
}

// make arrays have a default of an empty array
extension Array: Defaultable {
    static var defaultValue: Array { [] }
}

// make dictionaries have a default of an empty dictionary
extension Dictionary: Defaultable {
    static var defaultValue: Dictionary { [:] }
}

What SE-0280 allows us to do is exactly the same thing just for enums. For example, you want to create a padding enum that can take some number of pixels, some number of centimeters, or a default value decided by the system:

enum Padding: Defaultable {
    case pixels(Int)
    case cm(Int)
    case defaultValue
}

That kind of code wouldn’t have been possible before SE-0280 – Swift would have said that Padding doesn’t satisfy the protocol. However, if you think it through the protocol really is satisfied: we said it needs a static defaultValue that returns Self, i.e. whatever concrete type is conforming to the protocol, and that’s exactly what Padding.defaultValue does.

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).

Where next?

We’re like to see the first beta of Swift 5.3 shipping with Xcode Next at WWDC20, but in the meantime you can download a nightly toolchain snapshot from Swift.org.

I’d also suggest that you check out the Swift Standard Library Preview – an earlier version of this article featured SE-0270 that adds new collection methods on noncontiguous elements, but that has subsequently been moved to Library Preview. So, go and give it a try and see what you think!

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.