Introducing Swift 5.3
Introduced by Apple during 2020, the main goal in Swift 5.3 is to enhance quality and performance and to expand the number of platforms on which Swift is available by adding support for Windows and additional Linux distributions.
Now, let's review some of the new language features.
Multi-pattern catch clauses
With this new feature, Swift will allow multiple error-handling blocks inside a do catch
clause. Take a look at the following example.
Imagine that we have a performTask()
function that can throw different types of TaskError
errors:
enum TaskError: Error { case someRecoverableError case someFailure(msg: String) case anotherFailure(msg: String) } func performTask() throws -> String { throw TaskError.someFailure(msg: "Some Error") } func recover() {}
Prior to Swift 5.3, if we want to handle different TaskError
cases inside a do catch
block, we need to add a switch
statement inside the catch
clause, complicating the code, as follows:
do { try performTask() } catch let error as TaskError { switch error { case TaskError.someRecoverableError: recover() case TaskError.someFailure(let msg), TaskError.anotherFailure(let msg): print(msg) } }
Now Swift 5.3 allows us to define multiple catch
blocks so we can make our code more readable, as in the following example:
do { try performTask() } catch TaskError.someRecoverableError { recover() } catch TaskError.someFailure(let msg), TaskError.anotherFailure(let msg) { print(msg) }
We no longer need the switch
inside the catch
block.
Multiple trailing closures
Since the beginning of Swift, it has supported trailing closures syntax. See this classic example when using the UIView.animate
method:
UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }, completion: { _ in self.view.removeFromSuperview() })
Here, we were able to apply the trailing closure syntax to the completion
block to make our code shorter and more readable by extracting completion
from the parentheses and removing its label:
UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }) { _ in self.view.removeFromSuperview() }
This closure syntax has some side-effects too. It can make our code hard to read if a developer is not used to our methods (think about our own API library that is not as well known as UIKit methods). It also makes the code a bit unstructured.
With Swift 5.3, when we have multiple closures in the same method, we can now extract and label all of them after the first unlabeled parameter:
UIView.animate(withDuration: 0.3) { self.view.alpha = 0 } completion: { _ in self.view.removeFromSuperview() }
Notice how now we have both closures outside of the parentheses, UIView.animate(withDuration: 0.3)
. Also notice how labeling the completion
method makes it easier to understand, and how the code now looks more symmetrical in terms of structure, with all the closures written in the same way.
Synthesized comparable conformance for enum types
Swift 5.3 allow enum
types with no associated values or with only Comparable
values to be eligible for synthetized conformance. Let's see an example. Before Swift 5.3, if we wanted to compare the values of an enum
, we needed to conform to Comparable
, and we needed to implement <
and minimum
methods (among other ways to achieve this):
enum Volume: Comparable { case low case medium case high private static func minimum(_ lhs: Self, _ rhs: Self) -> Self { switch (lhs, rhs) { case (.low, _), (_, .low ): return .low case (.medium, _), (_, .medium): return .medium case (.high, _), (_, .high ): return .high } } static func < (lhs: Self, rhs: Self) -> Bool { return (lhs != rhs) && (lhs == Self.minimum(lhs, rhs)) } }
This code is hard to maintain; as soon as we add more values to the enum
, we need to update the methods again and again.
With Swift 5.3, as long as the enum doesn't have an associated value or it only has a Comparable
associated value, the implementation is synthesized for us. Check out the following example, in which we define an enum called Size
, and we are able to sort an array of Size
instances (without any further implementation of Comparable
methods):
enum Size: Comparable { case small(Int) case medium case large(Int) } let sizes: [Size] = [.medium, .small(1), .small(2), .large(0)]
If we print the array with print(sizes.sorted())
, we will get this in the console:
[.small(1), .small(2), .medium, .large(0)]
Note how the order of sorting is the same as the order in which we define our cases, assuming it is an increasing order: .small
appears before .large
when we sort the values. For instances of the same case that contain associated values (such as .small(Int)
and .large(Int)
) we apply the same principle when ordering: .small(1)
appears before .small(2)
.
Increase availability of implicit self in escaping closures when reference cycles are unlikely to occur
Sometimes the rule that forced all uses of self
in escaping closures to be explicit was adding boilerplate code. One example is when we are using closures within a Struct
(where the reference cycle is unlikely to occur). With this new change in Swift 5.3, we can omit self
, like in this example:
struct SomeStruct { var x = 0 func doSomething(_ task: @escaping () -> Void) { task() } func test() { doSomething { x += 1 // note no self.x } } }
There is also a new way to use self
in the capture list (just by adding [self] in
) when needed so that we can avoid using self
again and again inside the closures. See the following example:
class SomeClass { var x = 0 func doSomething(_ task: @escaping () -> Void) { task() } func test() { doSomething { [self] in x += 1 // instead of self.x += 1 x = x * 5 // instead of self.x = self.x * 5 } } }
This change reduces the use of self
in many situations and omits it completely when it is not needed.
Type-based program entry points – @main
Up until now, when developing a Swift program (such as a terminal app), we needed to define the program startup point in a main.swift
file. Now we are able to mark a struct or a base class (in any file) with @main
and a static func main()
method on it, and it will be triggered automatically when the program starts:
@main struct TerminalApp { static func main() { print("Hello Swift 5.3!") } }
Important note
Take into consideration the following about @main
: it should not be used if a main.swift
file already exists, it should be used in a base class (or struct), and it should only be defined once.
Use where clauses on contextually generic declarations
We can use where
clauses in functions with generic types and extensions. For example, look at the following code:
struct Stack<Element> { private var array = [Element]() mutating func push(_ item: Element) { array.append(item) } mutating func pop() -> Element? { array.popLast() } } extension Stack { func sorted() -> [Element] where Element: Comparable { array.sorted() } }
We constrained the sorted()
method on the extension of this Stack
struct to elements that are Comparable
.
Enum cases as protocol witnesses
This proposal aims to lift an existing restriction, which is that enum cases cannot participate in protocol witness matching. This was causing problems when conforming enums to protocol requirements. See the following example of a protocol that defines a maxValue
variable:
protocol Maximizable { static var maxValue: Self { get } }
We can make Int
conform to Maximizable
like this:
extension Int: Maximizable { static var maxValue: Int { Int.max } }
But if we try the same with an enum, we will have compile issues. Now it is possible to do this:
enum Priority: Maximizable { case minValue case someValue(Int) case maxValue }
This code now compiles properly with Swift 5.3.
Refine didSet semantics
This is a very straightforward change, according to the Swift proposal:
- If a
didSet
observer does not reference theoldValue
in its body, then the call to fetch theoldValue
will be skipped. We refer to this as a "simple"didSet
. - If we have a "simple"
didSet
and nowillSet
, then we could allow modifications to happen in-place.
Float16
Float16 has been added to the standard library. Float16 is a half-precision (16b) floating-point value type. Before Swift 5.3, we had Float32, Float64, and Float80.
In this section, you have learned about the latest additions to the language in Swift 5.3 with code examples. Now, let's finish with the chapter summary.