Multiple trailing closures, massive package manager improvements, and more.
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.
SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until October 1st.
Sponsor Hacking with Swift and reach the world's largest Swift community!
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.
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.
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 placesSE-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.
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
:
@main
attribute@main
attribute can be applied only to a base class – it will not be inherited by any subclasses.where
clauses on contextually generic declarationsSE-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()
}
}
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.
didSet
SemanticsSE-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
}
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 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)
.
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!
SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until October 1st.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.