Swift 5.3 - What's New

There’s been a lot of great strides made with the release of Swift 5.3. I’ll go over some of the updates – there’s been lots of great improvements! For full disclosure on everything that’s included, refer to the release notes.

Synthesized Comparable conformance for enum types

This means that enum declarations automatically get Comparable conformance for free. There are a few exceptions, however. A conformance will not be synthesized if a type has raw values, non recursively-conforming associated values or an explicit < implementation.

Here’s an example of this update:

enum Volume {
    case low
    case medium
    case high
}

([.high, .low, .medium] as [Volume]).sorted()
// Results in [Volume.low, Volume.medium, Volume.high]

Refined didSet Semantics

A performance improving change has been made to didSet semantics:

If a didSet observer does not reference the oldValue in its body, then the call to fetch the oldValue will be skipped.

Here’s an example of how these changes work:

struct Record: Equatable {
    let artistName: String
    let songName: String
}

class RecordLabel {
    var numberOfRecordsAdded: Int = 0 {
        didSet { print("didSet called") }
    }

    var records = [Record]()
    
    let maxNumberOfRecords: Int

    init(maxNumberOfRecords: Int) {
        self.maxNumberOfRecords = maxNumberOfRecords
    }

    func updateRecordCount(with newRecords: [Record]) {
        guard records.count < maxNumberOfRecords else {
            print("max number of records reached: \(maxNumberOfRecords)!")
            return
        }
        for record in newRecords {
            if !records.contains(record) {
                records.append(record)
                numberOfRecordsAdded += 1
            }
        }
    }
}

let recordLabel = RecordLabel(maxNumberOfRecords: 1000)
/// This calls the getter on 'numberOfRecordsAdded' even
/// though 'oldValue' is never referred to inside 
/// numberOfRecordsAdded's didSet
var records = [Record]()
for index in 0..<1000 {
    let record = Record(
        artistName: "artist\(index)",
        songName: "song\(index)"
    )
    records.append(record)
}
recordLabel.updateRecordCount(with: records)

With the improvements, now the oldValue in numberOfRecordsAdded won’t be accessed, so in the case where lots of new records are added, unneccessary expensive work is avoided.

Increase availability of implicit self in @escaping closures

There’s a lot more about this here, and I encourage you to read it to get the full set of changes.

The most obvious improvement is that this error is easier to handle:

error: reference to property 'x' in closure requires explicit 'self.' to make capture semantics explicit

Say for example, we created a class like this:

class TestClass {
    func longRunningAsyncCall(work: @escaping () -> Void) {
        work()
    }
}

Previously, when writing a closure, you’d have to reference every usage of self explicitly, like this:

longRunningAsyncCall {
    self.performOperation()
    self.performAnotherOperation()
    self.finish()
}

Now, you can add self in the closure capture list, and drop the other references:

longRunningAsyncCall { [self] in
    performOperation()
    performAnotherOperation()
    finish()
}

Additionally, if the type owning the closure is a value type (like a struct or enum), then you could write the closure like this:

longRunningAsyncCall {
    performOperation()
    performAnotherOperation()
    finish()
}

Personally, I’d always add [self] in the closure capture list for consistency, but this change makes it soo much cleaner when referencing self within closures. Lots of unnecessary code is made redundant.

Multi-Pattern Catch Clauses

This update makes it possible to handle multiple error cases in the catch portion of a try-catch statement.

Previously, if you wanted to handle multiple error cases, you’d have to do the following:

do {
    try createRecord()
} catch let error as RecordCreationError {
    switch error {
        case RecordCreationError.outOfSpace(let msg),
        case RecordCreationError.invalidArtist(let msg):
        displayMessageToUser(msg)
    }
}

But this is awkward, and it requires a boilerplate cast down to RecordCreationError.

Now you can write this:

do {
    try createRecord()
} catch RecordCreationError.outOfSpace(let msg),
        RecordCreationError.invalidArtist(let msg):
        displayMessageToUser(msg)
}

This is much neater and easier to read than the first example, and more naturally supports Swift’s extensive usage of pattern matching.

Multiple Trailing Closures

This change makes it so that multiple labeled trailing closures can follow an initial unlabeled trailing closure. If you’ve been an iOS Engineer for any period of time, you’ve probably come across the wonky UIView.animate API.

UIView.animate(withDuration: 0.3, animations: {
  self.view.alpha = 0
}, completion: { _ in
  self.view.removeFromSuperview()
})

Not only is this difficult to understand if you’re writing it for the first time, but when working in teams that often have rules around parameter alignment and closure syntax, these blocks of code can be a nightmare to maintain.

Even though the syntax is simplified with the first trailing closure, the second closure still has to be wrapped with the right parentheses ) before being syntactically valid.

With the improvements to the Swift language, the above can be written like this:

UIView.animate(withDuration: 0.3) {
  self.view.alpha = 0
} completion: { _ in
  self.view.removeFromSuperview()
}

The syntax is much cleaner and easier to follow, and you’re still able to understand the second closure is the completion handler.

Float16 Type

A new Float16 type has been introduced. This type is used extensively in GPU (graphics) and ML programming.

let float16Value: Float16 = 1

Conclusion

These are some of the improvements made to the Swift programming language in version 5.3. There’s a lot more to cover! To get the full list of changes, check here. Let me know if these examples have been helpful to your understanding, or if you have any suggestions in the comments section. Until next update!