Learn what's changed in Swift with hands-on code examples, all in one place and with no clutter.
Questions? Feedback? Tweet me @twostraws.
countElements()
is now count()
In Swift 1.0 you would count an array like this:
let items = [1, 2, 3]
println(countElements(items))
The countElements()
function has been renamed to count()
in Swift 1.1, so the new code is this:
let items = [1, 2, 3]
println(count(items))
Note: This has changed in later versions of Swift – count
is now a property of strings and collections.
@NSApplicationMain
iOS apps have a @UIApplicationMain
attribute that automatically generates a UIApplicationMain()
func to bootstrap the app. This is now also available to macOS developers using @NSApplicationMain
, and this attribute will automatically be added to Cocoa app delegates in all new projects.
zip()
function joins two sequencesIf you have two sequences that you'd like to join together, the zip()
function will do just that and return an array of tuples. For example:
let names = ["Sophie", "Charlotte", "John"]
let scores = [90, 92, 95]
let zipped = zip(names, scores)
That will output an array of the tuples ("Sophie", 90), ("Charlotte", 92), and ("John", 95).
flatMap()
method transforms optionals and arraysThe flatMap()
method is designed to allow you to transform optionals and elements inside a collection while also decreasing the amount of containment that happens. For example, if you transform an optional in a way that will also return an optional, using map()
would give you an optional optional (a double optional), whereas flatMap()
is able to combine those two optionals into a single optional.
let lengthOfFirst = names.first.flatMap { count($0) }
Decreasing the amount of containment also makes flatMap()
a simple way of converting multi-dimensional arrays into single-dimensional arrays:
[[1, 2], [3, 4], [5, 6]].flatMap { $0 }
There is no "map" operation there, so we're just left with the flattening behavior – that will result in a single array containing the value [1, 2, 3, 4, 5, 6]
.
@noescape
Closures are reference types, which means Swift must quietly add memory management calls when they are passed into functions. To avoid adding unwanted work, you can now mark closure parameters with the @noescape
keyword, which tells Swift the closure will be used before the function returns – it doesn't need to retain or release the closure.
As an example, this function checks whether a password that we have stored matches a password the user just entered, but it does this using a closure so that you can give it any encryption code you like. This closure is used immediately inside the function, so @noescape
may be used as a performance optimization:
func checkPassword(encryption: @noescape (String) -> ()) -> Bool {
if closure(enteredPassword) == storedPassword {
return true
} else {
return false
}
}
Note: This has changed in later versions of Swift – all closures are considered to be non-escaping by default.
Swift 1.2 gives us an alias for class final properties: static
. While class variables may be overridden in subclasses, static variables may not. For example:
class Student: Person {
// THIS ISN'T ALLOWED
override static var count: Int {
return 150
}
// THIS IS ALLOWED
override class var averageAge: Double {
return 19.5
}
}
In this usage, static var
is merely an alias for final class var
.
Constants may be set only once, but Swift 1.2 allows us to create constants without initializing them immediately. For example:
let username: String
if authenticated {
username = fetchUsername()
} else {
username = "Anonymous"
}
Set
data structureSwift 1.2 introduced a new Set
type that works similarly to NSSet
except with value semantics. Sets work similarly to arrays except they are not ordered and do not store any element more than once. For example:
var starships = Set<String>()
starships.insert("Serenity")
starships.insert("Enterprise")
starships.insert("Executor")
starships.insert("Serenity")
starships.insert("Serenity")
Even though that code tries to insert Serenity three times, it will only be stored in the set once.
Prior to Swift 1.2 bridging from Objective-C types to Swift types happened implicitly, meaning that you could use the two interchangeable. From Swift 1.2 onwards you must now use as
to typecast these yourself. For example:
authenticateUser(yourNSString as String)
Note: This has changed in later versions of Swift – implicit bridging never happens now.
if let
bindingsYou may now place multiple if let
bindings on a single line separated by a comma, rather than embed them in increasingly indented pyramids.
For example, previously you would write code like this:
if let user = loadUsername() {
if let password = decryptPassword() {
authenticate(user, password)
}
}
As of Swift 1.2 you can write this:
if let user = loadUsername(), let password = decryptPassword() {
authenticate(user, password)
}
as!
From Swift 1.2 onwards we have three ways of performing typecasts: as
is used for typecasts that will always succeed (e.g. someString as NSString
), as?
is used for typecasts that might fail (e.g. someView as? UIImageView
), and as!
is used to force typecasts. If you use as!
and you're wrong, your code will crash.
For example:
let submitButton = vw.viewWithTag(10) as! UIButton
try/catch
is a way of programming that means "try this thing, but if it fails do this other thing instead." Swift uses enums for error types so that it can ensure your error catching is exhaustive, just like with switch
statements. So for example, you might define your error list something like this:
enum MyError: ErrorType {
case UserError
case NetworkError
case DiscoverydError
}
Notice how my error type builds on the built-in ErrorType
protocol; this is required.
Once you've defined the various errors you want to work with, it's time to introduce three new keywords: throws
, try
, do
and catch
.
First up, throws
is a simple keyword that you add to your method to tell Swift it might fail. You put it right before where you put your method's return type, like this:
func doStuff() throws -> String {
Once that's done, you cannot call that method unless your code is written to handle any errors it throws – Xcode simply won't compile. When you want to throw an error from inside your methods, you just write throw
followed by the type of error you want to throw, like this:
func doStuff() throws -> String {
print("Do stuff 1")
print("Do stuff 2")
throw MyError.NetworkError
return "Some return value"
}
The dummy print()
calls are there so you can follow the program flow, as you'll see in a moment.
But first, on to the next keyword: try
. This is placed before any call to a method that throws an error, like this:
try doStuff()
This literally writes into your code "I acknowledge that this code might fail," so it's effectively syntactic sugar to ensure safety. But even with that your code still won't compile, because you don't catch the errors: you need to use do
and catch
.
Catching errors has two forms: catching specific errors and catching all errors. You can mix and match, meaning your code can say "if the error is X, I want to handle it like this; all other errors should be handled this other way."
Here's a very basic example showing how to catch all errors:
do {
try doStuff()
print("Success")
} catch {
print("An error occurred.")
}
If you remember, we made the doStuff()
method print "Do stuff 1" then "Do stuff 2" before throwing a network error. So, what will happen is:
doStuff()
method – its return statement will never be reachedcatch
blockTo be clear: in the code above, "Success" will never be printed – as soon as any try
methods throw an error, execution stops and jumps to the catch
block.
As I said, you can mix and match generic and specific catch
blocks, but you do need to be sure that all possible errors are caught. For example, this will execute one chunk of code for NetworkError errors, and another chunk for all other errors:
do {
try doStuff()
print("Success")
} catch MyError.NetworkError {
print("A network error occurred")
} catch {
print("An error occurred")
}
guard
keyword for early returnsIt's very common to place some conditional checks at the start of a method to ensure that various data is configured ready to go. For example, if a Submit button is tapped, you might want to check that the user has entered a username in your user interface. To do this, you'd use this code:
func submitTapped() {
guard username.text.characters.count > 0 else {
return
}
print("All good")
}
Using guard
might not seem much different to using if
, but with guard
your intention is clearer: execution should not continue if your conditions are not met. Plus it has the advantage of being shorter and more readable, so guard
is a real improvement, and I'm sure it will be adopted quickly.
There is one bonus to using guard
that might make it even more useful to you: if you use it to unwrap any optionals, those unwrapped values stay around for you to use in the rest of your code block. For example:
guard let unwrappedName = userName else {
return
}
print("Your username is \(unwrappedName)")
This is in comparison to a straight if
statement, where the unwrapped value would be available only inside the if
block, like this:
if let unwrappedName = userName {
print("Your username is \(unwrappedName)")
} else {
return
}
// this won't work – unwrappedName doesn't exist here!
print("Your username is \(unwrappedName)")
In Swift 2.2 the way strings are measured changed yet again. What was countElements()
became count()
in Swift 1.1, and in Swift 2.0 was removed entirely.
Instead, you should access the characters
property of your String, then call count
on that, like this:
let string = "Hello, Swift!"
let count = string.characters.count
print(count)
Note: This has changed in later versions of Swift – you should access the count
property of strings directly.
defer
keyword to delay work until your scope exitsSome languages have a concept of try/finally
which lets you tell your app "no matter what happens, I want this code to be executed." Swift 2 introduced its own take on this requirement using the defer
keyword: it means "I want this work to take place, but not just yet." In practice, this usually means the work will happen just before your method ends, but here's the cool thing: this will still happen if you throw an error.
First, a simple example:
override func viewDidLoad() {
super.viewDidLoad()
print("Checkpoint 1")
doStuff()
print("Checkpoint 4")
}
func doStuff() {
print("Checkpoint 2")
defer { print("Do clean up here") }
print("Checkpoint 3")
}
If you run that, you'll see "Checkpoint 1", "Checkpoint 2", "Checkpoint 3", "Do clean up here", then "Checkpoint 4". So, even though the defer
line appears before checkpoint 3, it gets executed after – it gets deferred until the method is about to end.
I put "Do clean up code here" in there because that's exactly what defer
is good at: when you know you need to flush a cache, write out a file or whatever, and you want to make sure that code gets executed regardless of what path is taken through your method.
As I said, work you schedule with defer
will execute no matter what route your code takes through your method, and that includes if you throw any errors. For example:
override func viewDidLoad() {
super.viewDidLoad()
print("Checkpoint 1")
do {
try doStuff()
} catch {
print("Error!")
}
print("Checkpoint 4")
}
func doStuff() throws {
print("Checkpoint 2")
defer { print("Do clean up here") }
throw MyError.UserError
print("Checkpoint 3")
}
As soon as doStuff()
throws its error, the method is exited and at that point the deferred code is called.
This is a simple change that is going to go a long way to help code readability. As you know, Swift developers prefer declaring things as constants (using let
) rather than variables (using var
). But what if you made something a variable by accident? Or if you thought you might need to change it, then never do?
As of Xcode 7 and Swift 2, you'll get warnings in your code whenever you declare variables that never change – Xcode literally examines the way you use the variable and knows if you never change it.
One regular problem that iOS developers hit is that we need to be careful when using new APIs – if you try and use UIStackView
on iOS 8, for example, your app will crash. In the olden days, Objective C developers would write code like this:
NSClassFromString(@"UIAlertController") != nil
That means, "if the UIAlertController class exists," which was a way of checking if we were running on iOS 8 or later. But because Xcode didn't know that was our goal, it couldn't ensure we got things right. Well, this is fixed with Swift 2, because you can now write code like this:
if #available(iOS 9, *) {
let stackView = UIStackView()
// do stuff
}
The magic happens with #available
: it will automatically check whether we are running on iOS 9 or later, and, if so, will run the code with the UIStackView
. The *
after "iOS 9" is there as a catch all for any future platforms that Apple introduces, and it's required.
So, #available
is cool, but even better is the fact that you can give it an else
block and, because Xcode now knows this block will only execute if the device is iOS 8 or earlier, it can warn you if you new APIs. For example, if you wrote something like this:
if #available(iOS 9, *) {
// do cool iOS 9 stuff
} else {
let stackView = UIStackView()
}
Using quote marks inside strings caused problems before Swift 2.1, which meant that using string literals inside string interpolation was also difficult. This has been fixed in Swift 2.1, so this kind of code works fine now:
print("Hello, \(username ?? "Anonymous")")
This means it's also possible to read dictionary keys using string interpolation, like this:
print("Hello, \(user["name"]!)")
++
and --
are deprecatedSwift 2.2 formally deprecates the ++
and --
operators, which means they still work but you'll get a warning when you use them. Deprecation is usually a first step towards removing something entirely, and in this case both of these operators will be removed in Swift 3.0.
In their place, you need to use += 1
and -= 1
instead. These operators have been there all along, and are not going away.
You might wonder why two long-standing operators are being removed, particularly when they exist in C, C#, Java, and – critically to its "joke" – C++. There are several answers, not least:
++
rather than += 1
is hardly a dramatic time saving++
doesn't have an obvious meaning to people learning Swift, whereas +=
at least reads as "add and assign."++
and --
were used – have also been deprecated, which brings me on to my next point…This changed outlawed the following syntax in Swift:
for var i = 1; i <= 10; i += 1 {
print("\(i) green bottles")
}
These are called C-style for loops because they have long been a feature of C-like languages, and conceptually even pre-date C by quite a long way.
Although Swift is (just about!) a C-like language, it has a number of newer, smarter alternatives to the traditional for loop. The result: this construct was deprecated in Swift 2.2 and will be removed "in a future version of Swift."
To replace these old for loops, use one of the many alternatives. For example, the "green bottles" code above could be rewritten to loop over a range, like this:
for i in 1...10 {
print("\(i) green bottles")
}
Remember, though, that it's a bad idea to create a range where the start is higher than the end: your code will compile, but it will crash at runtime. So, rather than writing this:
for i in 10...1 {
print("\(i) green bottles")
}
…you should write this instead:
for i in (1...10).reverse() {
print("\(i) green bottles")
}
Another alternative is just to use regular fast enumeration over an array of items, like this:
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for number in array {
print("\(number) green bottles")
}
Although if you want to be technically correct (also known as "the best kind of correct") you would write such a beast like this:
var array = Array(1...10)
for number in array {
print("\(number) green bottles")
}
A tuple is simply a comma-separated list of values, where each value may or may not be named. For example:
let singer = ("Taylor", "Swift")
let alien = ("Justin", "Bieber")
In older versions of Swift, you couldn't compare two tuples without writing some unwieldy code like this:
func == (t1: (T, T), t2: (T, T)) -> Bool {
return t1.0 == t2.0 && t1.1 == t2.1
}
It's not very user-friendly to require that kind of boilerplate code, and of course it would only work for tuples that have exactly two elements. In Swift 2.2, you no longer need to write that code because tuples can be compared directly:
let singer = ("Taylor", "Swift")
let alien = ("Justin", "Bieber")
if singer == alien {
print("Matching tuples!")
} else {
print("Non-matching tuples!")
}
Swift 2.2's automatic tuple comparison works with tuples with two elements just like the function we wrote, but it also works with tuples of other sizes – up to arity 6, which means a tuple that contains six elements.
(In case you were wondering: "arity" is pronounced like "arrity", but "tuple" is pronounced any number of ways: "toople", "tyoople" and "tupple" are all common.)
You can see how tuple comparison works by changing our two tuples like this:
let singer = ("Taylor", 26)
let alien = ("Justin", "Bieber")
Be prepared for a very long error message from Xcode, but the interesting part comes near the end:
note: overloads for '==' exist with these partially matching parameter lists: ......
((A, B), (A, B)), ((A, B, C), (A, B, C)), ((A, B, C, D), (A, B, C, D)), ((A, B, C, D, E), (A, B, C, D, E)), ((A, B, C, D, E, F), (A, B, C, D, E, F))
As you can see, Swift literally has functions to compare tuples all the way up to (A, B, C, D, E, F)
, which ought to be more than enough.
Another feature that has been deprecated is one that has been part of Swift since 2010 (yes, years before it launched). It's been named "the tuple splat", and not many people were using it. It's partly for that reason – although mainly because it introduces all sorts of ambiguities when reading code – that this syntax is being deprecated.
In case you were curious – and let's face it, you probably are – here's an example of tuple splat syntax in action:
func describePerson(name: String, age: Int) {
print("\(name) is \(age) years old")
}
let person = ("Taylor Swift", age: 26)
describePerson(person)
But remember: don't grow too fond of your new knowledge, because tuple splats are deprecated in Swift 2.2 and will be removed entirely in a later version.
Argument labels are a core feature of Swift, and let us write code like this:
for i in 1.stride(through: 9, by: 2) {
print(i)
}
Without the through
or by
labels, this code would lose its self-documenting nature: what do the 9 and 2 do in 1.stride(9, 2)
? In this example, Swift also uses the argument labels to distinguish 1.stride(through: 9, by: 2)
from 1.stride(to: 9, by: 2)
, which produces different results.
As of Swift 2.2, you can now use a variety of language keywords as these argument labels. You might wonder why this would be a good thing, but consider this code:
func printGreeting(name: String, repeat repeatCount: Int) {
for _ in 0 ..< repeatCount {
print(name)
}
}
printGreeting("Taylor", repeat: 5)
That uses repeat
as an argument label, which makes sense because the function will print a string a number of times. Because repeat
is a keyword, this code would not work before Swift 2.2 – you would need to write repeat
instead, which is unpleasant.
Note that there are still some keywords that may not be used, specifically var
, let
and inout
.
Another deprecation, but again with good reason: var
parameters are deprecated because they offer only marginal usefulness, and are frequently confused with inout
.
To give you an example, here is the printGreeting()
function modified to use var
:
func printGreeting(var name: String, repeat repeatCount: Int) {
name = name.uppercaseString
for _ in 0 ..< repeatCount {
print(name)
}
}
printGreeting("Taylor", repeat: 5)
The differences there are in the first two lines: name
is now var name
, and name
gets converted to uppercase so that "TAYLOR" is printed out five times.
Without the var
keyword, name
would have been a constant and so the uppercaseString
line would have failed.
The difference between var
and inout
is subtle: using var
lets you modify a parameter inside the function, whereas inout
causes your changes to persist even after the function ends.
As of Swift 2.2, var
is deprecated, and it's slated for removal in Swift 3.0. If this is something you were using, just create a variable copy of the parameter inside the method, like this:
func printGreeting(name: String, repeat repeatCount: Int) {
let upperName = name.uppercaseString
for _ in 0 ..< repeatCount {
print(upperName)
}
}
printGreeting("Taylor", repeat: 5)
#line
, #function
, #file
Swift 2.1 and earlier used the "screaming snake case" symbols __FILE__
, __LINE__
, __COLUMN__
, and __FUNCTION__
, which automatically get replaced the compiler by the filename, line number, column number and function name where they appear.
From Swift 2.2, those old symbols have been replaced with #file
, #line
, #column
and #function
, which will be familiar to you if you've already used Swift 2.0's #available to check for iOS features. As the official Swift review says, it also introduced "a convention where # means invoke compiler substitution logic here."
Here’s how the debug identifiers in action from Swift 2.2 and later:
func printGreeting(name: String, repeat repeatCount: Int) {
print("This is on line \(#line) of \(#function)")
let upperName = name.uppercaseString
for _ in 0 ..< repeatCount {
print(upperName)
}
}
printGreeting("Taylor", repeat: 5)
One unwelcome quirk of Swift before 2.2 was that selectors could be written as strings, like this:
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Tap!", style: .Plain, target: self, action: "buttonTaped")
If you look closely, I wrote "buttonTaped"
rather than "buttonTapped"
, but Xcode wasn't able to notify me of my mistake if either of those methods didn't exist.
This has been resolved as of Swift 2.2: using strings for selectors has been deprecated, and you should now write #selector(buttonTapped)
in that code above. If the buttonTapped()
method doesn't exist, you'll get a compile error – another whole class of bugs eliminated at compile time!
Swift 2.2 adds a new build configuration option that makes it easy to combine code code written in versions of Swift into a single file. This might seem unnecessary, but spare a thought to people who write libraries in Swift: do they target Swift 2.2 and hope everyone is using it, or target Swift 2.0 and hope users can upgrade using Xcode?
Using the new build option lets you write two different flavours of Swift, and the correct one will be compiled depending on the version of the Swift compiler.
For example:
#if swift(>=2.2)
print("Running Swift 2.2 or later")
#else
print("Running Swift 2.1 or earlier")
#endif
Just like the existing #if os()
build option, this adjusts what code is produced by the compiler: if you're using a Swift 2.2 compiler, the second print()
line won't even be seen. This means you can use utter gibberish if you want:
#if swift(>=2.2)
print("Running Swift 2.2 or later")
#else
THIS WILL COMPILE JUST FINE IF YOU'RE
USING A SWIFT 2.2 COMPILER BECAUSE
THIS BIT IS COMPLETELY IGNORED!
#endif
The way we call functions and methods already changed in Swift 2.0, but it's changing again and this time it's going to break everything. In Swift 2.x and earlier, method names did not require a label for their first parameter, so the name of the first parameter was usually built into the method name. For example:
names.indexOf("Taylor")
"Taylor".writeToFile("filename", atomically: true, encoding: NSUTF8StringEncoding)
SKAction.rotateByAngle(CGFloat(M_PI_2), duration: 10)
UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
override func numberOfSectionsInTableView(tableView: UITableView) -> Int
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView?
NSTimer.scheduledTimerWithTimeInterval(0.35, target: self, selector: #selector(createEnemy), userInfo: nil, repeats: true)
Swift 3 makes all labels required unless you specify otherwise, which means the method names no longer detail their parameters. In practice, this often means the last part of the method name gets moved to be the name of the first parameter.
To show you how that looks, here is that Swift 2.2 code followed by its equivalent in Swift 3:
names.indexOf("Taylor")
names.index(of: "Taylor")
"Taylor".writeToFile("filename", atomically: true, encoding: NSUTF8StringEncoding)
"Taylor".write(toFile: "somefile", atomically: true, encoding: String.Encoding.utf8)
SKAction.rotateByAngle(CGFloat(M_PI_2), duration: 10)
SKAction.rotate(byAngle: CGFloat(M_PI_2), duration: 10)
UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
UIFont.preferredFont(forTextStyle: UIFontTextStyle.subheadline)
override func numberOfSectionsInTableView(tableView: UITableView) -> Int
override func numberOfSections(in tableView: UITableView) -> Int
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView?
func viewForZooming(in scrollView: UIScrollView) -> UIView?
NSTimer.scheduledTimerWithTimeInterval(0.35, target: self, selector: #selector(createEnemy), userInfo: nil, repeats: true)
Timer.scheduledTimer(timeInterval: 0.35, target: self, selector: #selector(createEnemy), userInfo: nil, repeats: true)
In that last call, notice how NSTimer
is now just called Timer
. Several other basic types have also dropped the "NS" prefix, so you'll now see UserDefaults
, FileManager
, Data
, Date
, URL
URLRequest
, UUID
, NotificationCenter
, and more.
Those are methods you call, but this has a knock-on effect for many methods that get called too: when you're connecting to frameworks such as UIKit, they expect to follow the old-style "no first parameter name" rule even in Swift 3.
Here are some example signatures from Swift 2.2:
override func viewWillAppear(animated: Bool)
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
override func didMoveToView(view: SKView)
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?)
func textFieldShouldReturn(textField: UITextField) -> Bool
In Swift 3, they all need an underscore before the first parameter, to signal that the caller (Objective-C code) won't be using a parameter label:
override func viewWillAppear(_ animated: Bool)
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
override func didMoveToView(_ view: SKView)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
func textFieldShouldReturn(_ textField: UITextField) -> Bool
When Swift went open source in December 2015, its shiny new API guideliness contained three fateful words: "omit needless words." This introduced another huge raft of breaking changes in Swift 3, because it means that method names that contain self-evident words now have those words removed.
Let's look at some simple examples first. First, Swift 2.2:
let blue = UIColor.blueColor()
let min = numbers.minElement()
attributedString.appendAttributedString(anotherString)
names.insert("Jane", atIndex: 0)
UIDevice.currentDevice()
Can you identify the needless words? When you're working with UIColor
, of course blue is going to be a color, so saying blueColor()
is needless. When you append one attributed string to another, do you really need to specify that it's an attributed string you're appending as opposed to an elephant? And why should it be a method – surely a color should be a property!
Here is that same code in Swift 3:
let blue = UIColor.blue
let min = numbers.min()
attributedString.append(anotherString)
names.insert("Jane", at: 0)
UIDevice.current
As you can see, this makes method names significantly shorter!
This change has particularly affected strings, which had repetition all over the place. The best way to demonstrate this is to show before and after code side-by-side, so in the code below the first line of each pair is Swift 2.2 and the second is Swift 3.0:
" Hello ".stringByTrimmingCharactersInSet(.whitespaceAndNewlineCharacterSet())
" Hello ".trimmingCharacters(in: .whitespacesAndNewlines)
"Taylor".containsString("ayl")
"Taylor".contains("ayl")
"1,2,3,4,5".componentsSeparatedByString(",")
"1,2,3,4,5".components(separatedBy: ",")
myPath.stringByAppendingPathComponent("file.txt")
myPath.appendingPathComponent("file.txt")
"Hello, world".stringByReplacingOccurrencesOfString("Hello", withString: "Goodbye")
"Hello, world".replacingOccurrences(of: "Hello", with: "Goodbye")
"Hello, world".substringFromIndex(7)
"Hello, world".substring(from: 7)
"Hello, world".capitalizedString
"Hello, world".capitalized
Warning: capitalized
is still a property, but lowercaseString
and uppercaseString
have been transmogrified into the methods lowercased()
and uppercased()
.
I've chosen the examples so far because the jump to Swift 3 isn't vast, but there are quite a few changes that were significant enough to make my brain hit a speedbump – usually when the resulting method is so short that it wasn't immediately obvious what it was.
For example, look at this code:
dismiss(animated: true, completion: nil)
When I first saw that, I blanked: "dismiss what?" That's partly a result of the Stockholm syndrome that's inevitable having programmed for iOS for so long, but once you learn to reverse the parameter label change and re-add the needless words, you can see it's equivalent to this code in Swift 2.2:
dismissViewControllerAnimated(true, completion: nil)
In fact, the completion: nil
part is optional now, so you could even write this:
dismiss(animated: true)
A similar change happened to prepareForSegue()
, which now looks like this:
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?)
Although syntactically irrelevant, the capital letters we use to name classes and structs, properties, enums, and more have always followed a convention fairly closely: classes, structs, and enums use UpperCamelCase (MyStruct, WeatherType.Cloudy), properties and parameter names use lowerCamelCase (emailAddress, requestString).
I say "fairly closely" because there are some exceptions that are going to stop being exceptions in Swift 3: properties and parameters that started with initials in Swift 2.2 will now used lowerCamelCase in Swift 3.
Sometimes this isn't too strange: Swift 2.2 created NSURLRequest
objects using NSURLRequest(URL: someURL)
– note the capital "URL". Swift 3 rewrites that to URLRequest(url: someURL)
, and also means you'll use things like webView.request?.url?.absoluteString
for reading the URL of a web view.
Where it's a bit more jarring is when only part of the property name is in caps, e.g. CGColor
or CIColor
. Yes, you've guessed it: they become cgColor
and ciColor
in Swift 3, so you'll be writing code like this:
let red = UIColor.red.cgColor
This change does help drive consistency: all properties and parameters should start with a lowercase letter, no exceptions.
At the same time enum cases are also changing, moving from UpperCamelCase to lowerCamelCase. This makes sense: an enum is a data type (like a struct), but enum values are closer to properties. However, it does mean that wherever you've used an Apple enum, it will now be lowercase. So:
UIInterfaceOrientationMask.Portrait // old
UIInterfaceOrientationMask.portrait // new
NSTextAlignment.Left // old
NSTextAlignment.left // new
SKBlendMode.Replace // old
SKBlendMode.replace // new
You get the idea. However, this tiny change brings something much bigger because Swift's optionals are actually just an enum under the hood, like this:
enum Optional {
case None
case Some(Wrapped)
}
This means if you use .Some
to work with optionals, you'll need to switch to .some
instead. Of course, you could always take this opportunity to ditch .some
entirely – these two pieces of code are identical:
for case let .some(datum) in data {
print(datum)
}
for case let datum? in data {
print(datum)
}
Swift 3 introduced attributes for C functions that allow library authors to specify new and beautiful ways their code should be imported into Swift. For example, all those functions that start with "CGContext" now get mapped to properties methods on a CGContext object, which makes for much more idiomatic Swift. Yes, this means the hideous wart that is CGContextSetFillColorWithColor()
has finally been excised.
To demonstrate this, here's an example in Swift 2.2:
let ctx = UIGraphicsGetCurrentContext()
let rectangle = CGRect(x: 0, y: 0, width: 512, height: 512)
CGContextSetFillColorWithColor(ctx, UIColor.redColor().CGColor)
CGContextSetStrokeColorWithColor(ctx, UIColor.blackColor().CGColor)
CGContextSetLineWidth(ctx, 10)
CGContextAddRect(ctx, rectangle)
CGContextDrawPath(ctx, .FillStroke)
UIGraphicsEndImageContext()
In Swift 3 the CGContext
can be treated as an object that you can call methods on rather than repeating CGContext
again and again. So, we can rewrite that code like this:
if let ctx = UIGraphicsGetCurrentContext() {
let rectangle = CGRect(x: 0, y: 0, width: 512, height: 512)
ctx.setFillColor(UIColor.red.cgColor)
ctx.setStrokeColor(UIColor.black.cgColor)
ctx.setLineWidth(10)
ctx.addRect(rectangle)
ctx.drawPath(using: .fillStroke)
UIGraphicsEndImageContext()
}
Note: in both Swift 2.2 and Swift 3.0 UIGraphicsGetCurrentContext()
returns an optional CGContext
, but because Swift 3 uses method calls we need to safely unwrap before it's used.
This mapping of C functions exists elsewhere, for example you can now read the numberOfPages
property of a CGPDFDocument
, and CGAffineTransform
has been souped up quite dramatically. Here are some examples showing old and new:
CGAffineTransformIdentity
CGAffineTransform.identity
CGAffineTransformMakeScale(2, 2)
CGAffineTransform(scaleX: 2, y: 2)
CGAffineTransformMakeTranslation(128, 128)
CGAffineTransform(translationX: 128, y: 128)
CGAffineTransformMakeRotation(CGFloat(M_PI))
CGAffineTransform(rotationAngle: CGFloat(M_PI))
This is the part where some people will start to drift off in confusion, which is a shame because it's important.
Here's are some quotes from the Swift API guidelines:
Got that? It's no surprise that Swift's rules are expressed using lingustic terminology – it is after all a language! – but this at least gives me a chance to feel smug that I did a second degree in English. What it means is that many methods are changing names in subtle and sometimes confusing ways.
Let's start with a couple of simple examples:
myArray.enumerate()
myArray.enumerated()
myArray.reverse()
myArray.reversed()
Each time Swift 3 modifies the method by adding a "d" to the end: this is a value that's being returned.
These rules are mostly innocent enough, but it causes confusion when it comes to array sorting. Swift 2.2 used sort()
to return a sorted array, and sortInPlace()
to sort an array in place. In Swift 3.0, sort()
is renamed to sorted()
(following the examples above), and sortInPlace()
is renamed to sort()
.
TL;DR: This means you need to be careful because in Swift 2.2 sort()
returned a sorted array, but in Swift 3.0 sort()
sorts the array in place.
Swift lets us extend types using constraints, which is a powerful and expressive way to add functionality. To demonstrate this, let's look at a worked example in Swift 3.0 that modifies collections to do something trivial:
extension Collection where Iterator.Element: Comparable {
func lessThanFirst() -> [Iterator.Element] {
guard let first = self.first else { return [] }
return self.filter { $0 < first }
}
}
let items = [5, 6, 10, 4, 110, 3].lessThanFirst()
print(items)
That adds a new method called lessThanFirst()
, which returns all items in a collection that are less than the first item. So, using it with the array [5, 6, 10, 4, 110, 3]
will return [4, 3]
.
That code extends a protocol (Collection
) only where it matches a constraint: elements in the collection must conform to another protocol, Comparable
. This alone is powerful stuff, but let's take it back a step: what if we wanted something a bit more specific? Swift 3.0 lets us extend a concrete type rather than the protocol Collection
, so instead we could write this:
extension Array where Element: Comparable {
func lessThanFirst() -> [Element] {
guard let first = self.first else { return [] }
return self.filter { $0 < first }
}
}
let items = [5, 6, 10, 4, 110, 3].lessThanFirst()
print(items)
That extends a concrete type (only Array
) but still using a protocol for its constraint. What if we wanted to go even more specific – extend a concrete type with a concrete constraint, for example only arrays that contains integers? Well, it turns out that isn't possible in Swift 3.0, which usually strikes people as odd: if Swift 3.0 can handle extending protocols with another protocol as a constraint, then surely extending a specific type with a specific constraint should be a cinch?
Fortunately, this discrepancy has been removed in Swift 3.1, which means we can now write code like this:
extension Array where Element == Int {
func lessThanFirst() -> [Int] {
guard let first = self.first else { return [] }
return self.filter { $0 < first }
}
}
let items = [5, 6, 10, 4, 110, 3].lessThanFirst()
print(items)
That extends a concrete type (only Array
) and uses a concrete constraint (only where the elements are Int
).
Now, obviously we're using a trivial example here – in your own code this is going to be significantly more useful when you want to extend arrays containing your own custom structs.
Swift 3.0's support for nested types is useful to help you organize your data and increase encapsulation, but Swift 3.1 takes them to the next level by adding support for generics. Let's look at a simple example again, just to start with:
struct Message {
struct Attachment {
var contents: String
}
var title: String
var attachment: Attachment
}
That creates a Message
struct that has an Attachment
struct inside it – a nested type. I've added two String
properties, because messages will have some text and attachments will hold some text.
Now, what if we wanted either Message
or Attachment
to have different kinds of data – perhaps Int
or Data
? Well, that requires generics, so you might have found yourself writing something like this:
struct Message<T> {
struct Attachment {
var contents: String
}
var title: T
var attachment: Attachment
}
That tells Swift we want Message
to work across several data types, and whatever data type gets used to create the struct should also be used for the title
property. Or at least that's what it would tell Swift, if such code were actually legal – Swift 3.0 does not allow you to mix nested type with generics. Fortunately, this is exactly what Swift 3.1 allows, because nested types can now appear inside generic types.
Not content to stop there, Swift 3.1 takes this a step further: nested types can also be generic, either using their own generic type or by inheriting the generic type of their parent. For example:
struct Message<T> {
struct Attachment<T> {
var contents: T
}
var title: T
var attachment: Attachment<T>
}
With that code, the Message
struct will have a specific type assigned to it, and the Attachment
struct will always have the same type – you can't use String
for one and Int
for the other. So, this code will work fine:
let msg = Message(title: "Hello", attachment: Message.Attachment(contents: "World"))
Helpfully, if your goal is to make the nested type and its container use the same generic type, you don't even need to declare the nested type as generic – Swift makes the outer type available to the nested type, so in fact you can just write this:
struct Message<T> {
struct Attachment {
var contents: T
}
var title: T
var attachment: Attachment
}
Generics are great and so are nested types, so I'm really pleased to see Swift 3.1 bring them together at last.
prefix(while:)
and drop(while:)
methodsTwo useful new methods have been added to the Sequence
protocol: prefix(while:)
and drop(while:)
. The former returns the longest subsequence that satisfies a predicate, which is a fancy way of saying that you give it a closure to run on every item, and it will go through all the elements in the sequence and return those that match the closure – but will stop as soon as it finds a non-matching element.
Let's take a look at a code example:
let names = ["Michael Jackson", "Michael Jordan", "Michael Caine", "Taylor Swift", "Adele Adkins", "Michael Douglas"]
let prefixed = names.prefix { $0.hasPrefix("Michael") }
print(prefixed)
That uses the hasPrefix()
method to return the subsequence ["Michael Jackson", "Michael Jordan", "Michael Caine"
– the first three elements in the sequence. It won't include "Michael Douglas", because that comes after the first non-Michael. If you wanted all the Michaels regardless of their position, you should use filter()
instead.
The second new method, drop(while:)
is effectively the opposite: it finds the longest subsequence that satisfies your predicate, then returns everything after it. For example:
let names = ["Michael Jackson", "Michael Jordan", "Michael Caine", "Taylor Swift", "Adele Adkins", "Michael Douglas"]
let dropped = names.drop { $0.hasPrefix("Michael") }
print(dropped)
That will return the subsequence ["Taylor Swift", "Adele Adkins", "Michael Douglas"]
– everything after the initial Michaels.
We know value types are great, but we also know they interact terribly with Objective-C APIs such as NSCoding
– you either need to write a shim layer or give in and use classes, both of which are unpleasant. Worse, even if you give in and switch to classes, you still need to write your encoding and decoding methods by hand, which is painful and error-prone.
Swift 4 introduced a new Codable
protocol that lets you serialize and deserialize custom data types without writing any special code – and without having to worry about losing your value types. Even better, you can choose how you want the data to be serialized: you can use classic property list format or even JSON.
Let's take a look at how beautiful this is. First, here's a custom data type and some instances of it:
struct Language: Codable {
var name: String
var version: Int
}
let swift = Language(name: "Swift", version: 4)
let php = Language(name: "PHP", version: 7)
let perl = Language(name: "Perl", version: 6)
You can see I've marked the Language
struct as conforming to the Codable
protocol. With that one tiny addition, we can convert it to a Data
representation of JSON like this:
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(swift) {
// save `encoded` somewhere
}
Swift will automatically encode all properties inside your data type – you don't need to do anything.
Now, if you're like me and have a long history of using NSCoding
, you're probably somewhat doubtful: is that really all it takes, and how can we be sure it's working? Well, let's add some more code to try converting the Data
object into a string so we can print it out, then decode it back into a new Language
instance that we can read from:
if let encoded = try? encoder.encode(swift) {
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Language.self, from: encoded) {
print(decoded.name)
}
}
Notice how decoding doesn't require a typecast – you provide the data type name as its first parameter, so Swift infers the return type from there.
Both JSONEncoder
and its property list counterpart PropertyListEncoder
have lots of options for customizing how they work: do you want compact JSON or pretty-printed JSON? Do you want to use ISO8601 dates or Unix epoch dates? Do you want to use binary property lists or XML? For more information on these and other options, see the Swift Evolution proposal for this new feature.
Writing multi-line strings in Swift has always meant adding \n
inside your strings to add line breaks wherever you want them. This doesn't look good in code, but at least it displays correctly for users. Fortunately, Swift 4 introduced new multi-line string literal syntax that lets you add line breaks freely and use quote marks without escaping, while still benefiting from functionality like string interpolation.
To start a string literal, you need to write three double quotation marks: """
then press return. You can then go ahead and write a string as long as you want, including variables and line breaks, before ending your string by pressing return then writing three more double quotation marks.
String literals have two important rules: when you open a string using """
the content of your string must begin on a new line, and when you end a multi-line string using """
that must also begin on a new line.
Here it is in action:
let longString = """
When you write a string that spans multiple
lines make sure you start its content on a
line all of its own, and end it with three
quotes also on a line of their own.
Multi-line strings also let you write "quote marks"
freely inside your strings, which is great!
"""
That creates a new string with several line breaks right there in the definition – much easier to read and write.
For more information see the Swift Evolution proposal for this new feature.
One of the most loved features of Objective-C is its ability to reference a property dynamically rather than directly – that is, to be able to say "given object X, here is the property I'd like to read" without actually reading it. These references, called keypaths, are distinct from direct property accesses because they don't actually read or write the value, they just stash it away for use later on.
If you've never used keypaths before, let me show you an analogy of how they work using regular Swift methods. We're going to define a struct called Starship
and a struct called Crew
, then create one instance of each:
// an example struct
struct Crew {
var name: String
var rank: String
}
// another example struct, this time with a method
struct Starship {
var name: String
var maxWarp: Double
var captain: Crew
func goToMaximumWarp() {
print("\(name) is now travelling at warp \(maxWarp)")
}
}
// create instances of those two structs
let janeway = Crew(name: "Kathryn Janeway", rank: "Captain")
let voyager = Starship(name: "Voyager", maxWarp: 9.975, captain: janeway)
// grab a reference to the `goToMaximumWarp()` method
let enterWarp = voyager.goToMaximumWarp
// call that reference
enterWarp()
Because functions are first-class types in Swift, the last two lines are able to create a reference to the goToMaximumWarp()
method called enterWarp
, then call that later on whenever we want to. The problem is, you can't do the same thing for properties – you can't say "create a reference to the captain's name property that I can check when the inevitable mutiny happens," because Swift will just read the property directly and you'll just get its original value.
This is fixed with keypaths: they are uninvoked references to properties just like our enterWarp()
code. If you invoke the reference now you get the current value, but if you invoke the reference later you get the latest value. You can dig through any number of properties, and Swift uses its type inference to ensure you get the correct type back.
The Swift Evolution community spent quite a while discussing the correct syntax for keypaths because it needed to be something visually different from other Swift code, and the syntax they ended up with uses backslashes: \Starship.name
, \Starship.maxWarp
, and \Starship.captain.name
. You can assign those two to a variable then use them whenever you want, on any Starship
instance. For example:
let nameKeyPath = \Starship.name
let maxWarpKeyPath = \Starship.maxWarp
let captainName = \Starship.captain.name
let starshipName = voyager[keyPath: nameKeyPath]
let starshipMaxWarp = voyager[keyPath: maxWarpKeyPath]
let starshipCaptain = voyager[keyPath: captainName]
That will make starshipName
a string and starshipMaxWarp
a double, because Swift is able to infer the types correctly. The third example there even goes into the property of a property, and Swift still figures it out correctly.
Future plans for this include being able to access array indexes and to create keypaths from strings at runtime – for more information see the Swift Evolution proposal for this new feature.
One of the most intriguing proposals for Swift 4 was to add some new functionality to dictionaries to make them more powerful, and also to make them behave more like you would expect in certain situations.
Let's start with a simple example: filtering dictionaries in Swift 3 does not return a new dictionary. Instead, it returns an array of tuples with key/value labels. For example:
let cities = ["Shanghai": 24_256_800, "Karachi": 23_500_000, "Beijing": 21_516_000, "Seoul": 9_995_000];
let massiveCities = cities.filter { $0.value > 10_000_000 }
After that code runs you can't read massiveCities["Shanghai"]
because it is no longer a dictionary. Instead, you need to use massiveCities[0].value
, which isn't great.
As of Swift 4 this behaves more like you would expect: you get back a new dictionary. Obviously this will break any existing code that relies on the tuple-array return type.
Similarly, the map()
method on dictionaries never quite worked the way many people hoped: you got a key-value tuple passed in, and could return a single value to be added to an array. For example:
let populations = cities.map { $0.value * 2 }
That hasn't changed in Swift 4, but there is a new method called mapValues()
that is going to be much more useful because it lets you transform the values and place them back into a dictionary using the original keys.
For example, this code will round and stringify all city populations, then put them back into a new dictionary with the same keys of Shanghai, Karachi, and Seoul:
let roundedCities = cities.mapValues { "\($0 / 1_000_000) million people" }
(In case you were wondering, it's not safe to map dictionary keys because you might create duplicates by accident.)
Easily my favorite new dictionary addition is a grouping
initializer, which converts a sequence into a dictionary of sequences that are grouped by whatever you want. Continuing our cities
example, we could use cities.keys
to get back an array of city names, then group them by their first letter, like this:
let groupedCities = Dictionary(grouping: cities.keys) { $0.characters.first! }
print(groupedCities)
That will output the following:
["B": ["Beijing"], "S": ["Shanghai", "Seoul"], "K": ["Karachi"]]
Alternatively, we could group the cities based on the length of their names like this:
let groupedCities = Dictionary(grouping: cities.keys) { $0.count }
print(groupedCities)
That will output the following:
[5: ["Seoul"], 7: ["Karachi", "Beijing"], 8: ["Shanghai"]]
Finally, it's now possible to access a dictionary key and provide a default value to use if the key is missing:
let person = ["name": "Taylor", "city": "Nashville"]
let name = person["name", default: "Anonymous"]
Now, any experienced developer will probably argue that's better written using nil coalescing, and I agree. You could write this line instead using the current version of Swift:
let name = person["name"] ?? "Anonymous"
However, that doesn't work when you're modifying the dictionary value rather than just reading it. You can't modify a dictionary value in place because accessing its key returns an optional – the key might not exist, after all. With Swift 4's default dictionary values you can write much more succinct code, such as this:
var favoriteTVShows = ["Red Dwarf", "Blackadder", "Fawlty Towers", "Red Dwarf"]
var favoriteCounts = [String: Int]()
for show in favoriteTVShows {
favoriteCounts[show, default: 0] += 1
}
That loops over every string in favoriteTVShows
, and uses a dictionary called favoriteCounts
to keep track of how often each item appears. We can modify the dictionary in one line of code because we know it will always have a value: either the default value of 0, or some higher number based on previous counting.
For more information see the Swift Evolution proposal for these new features.
This is a small change, but one guaranteed to make a lot of people happy: strings are collections again. This means you can reverse them, loop over them character-by-character, map()
and flatMap()
them, and more. For example:
let quote = "It is a truth universally acknowledged that new Swift versions bring new features."
let reversed = quote.reversed()
for letter in quote {
print(letter)
}
This change was introduced as part of a broad set of amendments called the String Manifesto.
Last but not least, Swift 4 introduced Python-like one-sided collection slicing, where the missing side is automatically inferred to be the start or end of the collection. This has no effect on existing code because it's a new use for the existing operator, so you don't need to worry about potential breakage.
Here's an example:
let characters = ["Dr Horrible", "Captain Hammer", "Penny", "Bad Horse", "Moist"]
let bigParts = characters[..<3]
let smallParts = characters[3...]
print(bigParts)
print(smallParts)
That code will print out ["Dr Horrible", "Captain Hammer", "Penny"]
then ["Bad Horse", "Moist"]
.
For more information see the Swift Evolution proposal for this new feature.
The Equatable
protocol allows Swift to compare one instance of a type against another. When we say 5 == 5
, Swift understands what that means because Int
conforms to Equatable
, which means it implements a function describing what ==
means for two instances of Int
.
Implementing Equatable
in our own value types allows them to work like Swift’s strings, arrays, numbers, and more, and it’s usually a good idea to make your structs conform to Equatable
just so they fit the concept of value types better.
However, implementing Equatable
can be annoying. Consider this code:
struct Person {
var firstName: String
var lastName: String
var age: Int
var city: String
}
If you have two instances of Person
and want to make sure they are identical, you need to compare all four properties, like this:
struct Person: Equatable {
var firstName: String
var lastName: String
var age: Int
var city: String
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.age == rhs.age && lhs.city == rhs.city
}
}
Even reading that is tiring, never mind writing it.
Fortunately, Swift 4.1 can synthesize conformance for Equatable
– it can generate an ==
method automatically, which will compare all properties in one value with all properties in another, just like above. So, all you have to do now is add Equatable
as a protocol for your type, and Swift will do the rest.
Of course, if you want you can implement ==
yourself. For example, if your type has an id
field that identifies it uniquely, you would write ==
to compare that single value rather than letting Swift do all the extra work.
Swift 4.1 also introduced synthesized support for the Hashable
protocol, which means it will generate a hashValue
property for conforming types automatically. Hashable
was always annoying to implement because you need to return a unique (or at least mostly unique) hash for every object. It’s important, though, because it lets you use your objects as dictionary keys and store them in sets.
Previously we’d need to write code like this:
var hashValue: Int {
return firstName.hashValue ^ lastName.hashValue &* 16777619
}
For the most part that’s no longer needed in Swift 4.1, although as with Equatable
you might still want to write your own method if there’s something specific you need.
Note: You still need to opt in to these protocols by adding a conformance to your type, and using the synthesized code does require that all properties in your type conform to Equatable
or Hashable
respectively.
For more information, see Swift Evolution proposal SE-0185.
Codable
In Swift 4.0 a common problem was trying to use Codable
with JSON that utilized snake_case for its key names rather than the camelCase we normally use in Swift. Codable was unable to understand how the two different name types were mapped, so you had to create a custom CodingKeys
enum helping it out.
This is where Swift 4.1's new keyDecodingStrategy
property comes in: it’s set to .useDefaultKeys
by default, which does a direct mapping of JSON names to Swift properties. However, if you change it to .convertFromSnakeCase
then Codable
handles the name conversion for us.
For example:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let macs = try decoder.decode([Mac].self, from: jsonData)
print(macs)
} catch {
print(error.localizedDescription)
}
When you want to go back the other way – to convert a Codable
struct with camelCase properties back to JSON with snake_case keys, set the keyEncodingStrategy
to .convertToSnakeCase
like this:
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encoded = try encoder.encode(someObject)
Note: At the time of writing keyDecodingStrategy
and keyEncodingStrategy
are not available on Linux.
Swift 4.1 implements SE-0143, which introduced proposed conditional conformances into the language. This allows types to conform to a protocol only when certain conditions are met.
To demonstrate conditional conformances, let's create a Purchaseable
protocol that we can use to buy things:
protocol Purchaseable {
func buy()
}
We can now define a Book
struct that conforms to the protocol, and prints a message when a book is bought:
struct Book: Purchaseable {
func buy() {
print("You bought a book")
}
}
So far this is easy enough, but let's take it one step further: what if the user has a basket full of books, and wants to buy them all? We could loop over all books in the array by hand, calling buy()
on each one. But a better approach is to write an extension on Array
to make it conform to Purchaseable
, then give it a buy()
method that in turn calls buy()
on each of its elements.
This is where conditional conformances come in: if we tried to extend all arrays, we'd be adding functionality where it wouldn't make sense – we'd be adding buy()
to arrays of strings, for example, even though those strings don't have a buy()
method we can call.
Swift 4.1 lets us make arrays conform to Purchaseable
only if their elements also conform to Purchaseable
, like this:
extension Array: Purchaseable where Element: Purchaseable {
func buy() {
for item in self {
item.buy()
}
}
}
As you can see, conditional conformances let us constrain the way our extensions are applied more precisely than was possible before.
Conditional conformances also make large parts of Swift code easier and safer, even if you don't do any extra work yourself. For example, this code creates two arrays of optional strings and checks whether they are equal:
var left: [String?] = ["Andrew", "Lizzie", "Sophie"]
var right: [String?] = ["Charlotte", "Paul", "John"]
left == right
That might seem trivial, but that code wouldn't even compile in Swift 4.0 – both String
and [String]
were equatable, but [String?]
was not.
The introduction of conditional conformance in Swift 4.1 means that it’s now possible to add protocol conformance to a type as long as it satisfies a condition. In this case, if the elements of the array are equatable, that means the whole thing is equatable. So, the above code now compiles in Swift 4.1
Conditional conformance has been extended to the Codable
protocol in a way that will definitely make things safer. For example:
struct Person {
var name = "Taylor"
}
var people = [Person()]
var encoder = JSONEncoder()
// try encoder.encode(people)
If you uncomment the encoder.encode(people)
line, Swift will refuse to build your code because you're trying to encode a struct that doesn't conform to Codable
. However, that code compiled cleanly with Swift 4.0, then threw a fatal error at runtime because Person
doesn’t conform to Codable
.
Obviously no one wants a fatal error at runtime, because it means your app crashes. Fortunately, Swift 4.1 cleans this up using conditional conformances: Optional
, Array
, Dictionary
, and Set
now only conform to Codable
if their contents also conform to Codable
, so the above code will refuse to compile.
Swift 4.1 implements SE-0157, which lifts restrictions on the way we use associated types inside protocols. As a result, we can now create recursive constraints for our associated types: associated types that are constrained by the protocol they are defined in.
To demonstrate this, let's consider a simple team hierarchy in a tech company. In this company, every employee has a manager – someone more senior to them that they report to. Each manager must also be an employee of the company, because it would be weird if they weren't.
We can express this relationship in a simple Employee
protocol:
protocol Employee {
associatedtype Manager: Employee
var manager: Manager? { get set }
}
Note: I've used an optional Manager?
because ultimately one person (presumably the CEO) has no manager.
Even though that's a fairly self-evident relationship, it wasn't possible to compile that code in Swift 4.0 because we're using the Employee
protocol inside itself. However, this is fixed in Swift 4.1 because of the new ability to use recursive constraints on associated types.
Thanks to this new feature, we can model a simple tech company that has three kinds of team members: junior developers, senior developers, and board members. The reporting structure is also simple: junior developers are managed by senior developers, senior developers are managed by board members, and board members may be managed by another board member – e.g. the CTO reporting to the CEO.
That looks exactly as you would imagine thanks to Swift 4.1:
class BoardMember: Employee {
var manager: BoardMember?
}
class SeniorDeveloper: Employee {
var manager: BoardMember?
}
class JuniorDeveloper: Employee {
var manager: SeniorDeveloper?
}
Note: I've used classes here rather than structs because BoardMember
itself contains a BoardMember
property and that would result in an infinitely sized struct. If one of these has to be a class I personally would prefer to make all three classes just for consistency, but if you preferred you could leave BoardMember
as a class and make both SeniorDeveloper
and JuniorDeveloper
into structs.
Swift 4.1 implemented SE-0075, which introduced a new canImport
condition that lets us check whether a specific module can be imported when our code is compiled.
This is particularly important for cross-platform code: if you had a Swift file that implemented one behavior on macOS and another on iOS, or if you needed specific functionality for Linux. For example:
#if canImport(SpriteKit)
// this will be true for iOS, macOS, tvOS, and watchOS
#else
// this will be true for other platforms, such as Linux
#endif
Previously you would have had to use inclusive or exclusive tests by operating system, like this:
#if !os(Linux)
// Matches macOS, iOS, watchOS, tvOS, and any other future platforms
#endif
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
// Matches only Apple platforms, but needs to be kept up to date as new platforms are added
#endif
The new canImport
condition lets us focus on the functionality we care about rather than what platform we're compiling for, thus avoiding a variety of problems.
Swift 4.1 implemented SE-0190, which introduced a new targetEnvironment
condition that lets us differentiate between builds that are for physical devices and those that are for a simulated environment.
At this time targetEnvironment
has only one value, simulator
, which will be true if your build is targeting a simulated device such as the iOS Simulator. For example:
#if targetEnvironment(simulator)
// code for the simulator here
#else
// code for real devices here
#endif
This is useful when writing code to deal with functionality the simulator doesn't support, such as capturing photos from a camera or reading the accelerometer.
As an example, let's look at processing a photo from the camera. If we're running on a real device we'll create and configure a UIImagePickerController()
to take photos using the camera, but if we're in the simulator we'll just load a sample image from our app bundle:
import UIKit
class TestViewController: UIViewController, UIImagePickerControllerDelegate {
// a method that does some sort of image processing
func processPhoto(_ img: UIImage) {
// process photo here
}
// a method that loads a photo either using the camera or using a sample
func takePhoto() {
#if targetEnvironment(simulator)
// we're building for the simulator; use the sample photo
if let img = UIImage(named: "sample") {
processPhoto(img)
} else {
fatalError("Sample image failed to load")
}
#else
// we're building for a real device; take an actual photo
let picker = UIImagePickerController()
picker.sourceType = .camera
vc.allowsEditing = true
picker.delegate = self
present(picker, animated: true)
#endif
}
// this is called if the photo was taken successfully
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// hide the camera
picker.dismiss(animated: true)
// attempt to retrieve the photo they took
guard let image = info[UIImagePickerControllerEditedImage] as? UIImage else {
// that failed; bail out
return
}
// we have an image, so we can process it
processPhoto(image)
}
}
flatMap
is now (partly) compactMap()
The flatMap()
method was useful for a variety of things in Swift 4.0, but one was particularly useful: the ability to transform each object in a collection, then remove any items that were nil.
Swift Evolution proposal SE-0187 suggested changing this, and as of Swift 4.1 this flatMap()
variant has been renamed to compactMap()
to make its meaning clearer.
For example:
let array = ["1", "2", "Fish"]
let numbers = array.compactMap { Int($0) }
That will create an Int
array containing the numbers 1 and 2, because "Fish" will fail conversion to Int
, return nil, and be ignored.
SE-0194 introduced a new CaseIterable
protocol that automatically generates an array property of all cases in an enum.
Prior to Swift 4.2 this either took hacks, hand-coding, or Sourcery code generation to accomplish, but now all you need to do is make your enum conform to the CaseIterable
protocol. At compile time, Swift will automatically generate an allCases
property that is an array of all your enum’s cases, in the order you defined them.
For example, this creates an enum of pasta shapes and asks Swift to automatically generate an allCases
array for it:
enum Pasta: CaseIterable {
case cannelloni, fusilli, linguine, tagliatelle
}
You can then go ahead and use that property as a regular array – it will be a [Pasta]
given the code above, so we could print it like this:
for shape in Pasta.allCases {
print("I like eating \(shape).")
}
This automatic synthesis of allCases
will only take place for enums that do not use associated values. Adding those automatically wouldn’t make sense, however if you want you can add it yourself:
enum Car: CaseIterable {
static var allCases: [Car] {
return [.ford, .toyota, .jaguar, .bmw, .porsche(convertible: false), .porsche(convertible: true)]
}
case ford, toyota, jaguar, bmw
case porsche(convertible: Bool)
}
At this time, Swift is unable to synthesize the allCases
property if any of your enum cases are marked unavailable. So, if you need allCases
then you’ll need to add it yourself, like this:
enum Direction: CaseIterable {
static var allCases: [Direction] {
return [.north, .south, .east, .west]
}
case north, south, east, west
@available(*, unavailable)
case all
}
Important: You need to add CaseIterable
to the original declaration of your enum rather than an extension in order for the allCases
array to be synthesized. This means you can’t use extensions to retroactively make existing enums conform to the protocol.
SE-0196 introduced new compiler directives that help us mark issues in our code. These will be familiar to any developers who had used Objective-C previously, but as of Swift 4.2 we can enjoy them in Swift too.
The two new directives are #warning
and #error
: the former will force Xcode to issue a warning when building your code, and the latter will issue a compile error so your code won’t build at all. Both of these are useful for different reasons:
#warning
is mainly useful as a reminder to yourself or others that some work is incomplete. Xcode templates often use #warning
to mark method stubs that you should replace with your own code.#error
is mainly useful if you ship a library that requires other developers to provide some data. For example, an authentication key for a web API – you want users to include their own key, so using #error
will force them to change that code before continuing.Both of these work in the same way: #warning("Some message")
and #error("Some message")
. For example:
func encrypt(_ string: String, with password: String) -> String {
#warning("This is terrible method of encryption")
return password + String(string.reversed()) + password
}
struct Configuration {
var apiKey: String {
#error("Please enter your API key below then delete this line.")
return "Enter your key here"
}
}
Both #warning
and #error
work alongside the existing #if
compiler directive, and will only be triggered if the condition being evaluated is true. For example:
#if os(macOS)
#error("MyLibrary is not supported on macOS.")
#endif
SE-0195 introduced a way to bring Swift closer to scripting languages such as Python, but in a type-safe way – you don’t lose any of Swift’s safety, but you do gain the ability to write the kind of code you’re more likely to see in PHP and Python.
At the core of this feature is a new attribute called @dynamicMemberLookup
, which instructs Swift to call a subscript method when accessing properties. This subscript method, subscript(dynamicMember:)
, is required: you’ll get passed the string name of the property that was requested, and can return any value you like.
Let’s look at a trivial example so you can understand the basics. We could create a Person
struct that reads its values from a dictionary like this:
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["name": "Taylor Swift", "city": "Nashville"]
return properties[member, default: ""]
}
}
The @dynamicMemberLookup
attribute requires the type to implement a subscript(dynamicMember:)
method to handle the actual work of dynamic member lookup. As you can see, I’ve written one that accepts the member name as string and returns a string, and internally it just looks up the member name in a dictionary and returns its value.
That struct allows us to write code like this:
let person = Person()
print(person.name)
print(person.city)
print(person.favoriteIceCream)
That will compile cleanly and run, even though name
, city
, and favoriteIceCream
do not exist as properties on the Person
type. Instead, they are all looked up at runtime: that code will print “Taylor Swift” and “Nashville” for the first two calls to print()
, then an empty string for the final one because our dictionary doesn’t store anything for favoriteIceCream
.
My subscript(dynamicMember:)
method must return a string, which is where Swift’s type safety comes in: even though you’re dealing with dynamic data, Swift will still ensure you get back what you expected. And if you want multiple different types, just implement different subscript(dynamicMember:)
methods, like this:
@dynamicMemberLookup
struct Employee {
subscript(dynamicMember member: String) -> String {
let properties = ["name": "Taylor Swift", "city": "Nashville"]
return properties[member, default: ""]
}
subscript(dynamicMember member: String) -> Int {
let properties = ["age": 26, "height": 178]
return properties[member, default: 0]
}
}
Now that any property can be accessed in more than one way, Swift requires you to be clear which one should be run. That might be implicit, for example if you send the return value into a function that accepts only strings, or it might be explicit, like this:
let employee = Employee()
let age: Int = employee.age
Either way, Swift must know for sure which subscript will be called.
You can even overload subscript
to return closures:
@dynamicMemberLookup
struct User {
subscript(dynamicMember member: String) -> (_ input: String) -> Void {
return {
print("Hello! I live at the address \($0).")
}
}
}
let user = User()
user.printAddress("555 Taylor Swift Avenue")
When that’s run, user.printAddress
returns a closure that prints out a string, and the ("555 Taylor Swift Avenue")
part immediately calls it with that input.
If you use dynamic member subscripting in a type that has also some regular properties and methods, those properties and methods will always be used in place of the dynamic member. For example, we could define a Singer
struct with a built-in name
property alongside a dynamic member subscript:
struct Singer {
public var name = "Justin Bieber"
subscript(dynamicMember member: String) -> String {
return "Taylor Swift"
}
}
let singer = Singer()
print(singer.name)
That code will print “Justin Bieber”, because the name
property will be used rather than the dynamic member subscript.
@dynamicMemberLookup
plays a full part in Swift’s type system, which means you can assign them to protocols, structs, enums, and classes – even classes that are marked @objc
.
In practice, this means two things. First, you can create a class using @dynamicMemberLookup
, and any classes that inherit from it are also automatically @dynamicMemberLookup
. So, this will print “I’m a sandwich” because HotDog
inherits from Sandwich
:
@dynamicMemberLookup
class Sandwich {
subscript(dynamicMember member: String) -> String {
return "I'm a sandwich!"
}
}
class HotDog: Sandwich { }
let chiliDog = HotDog()
print(chiliDog.description)
Second, you can retroactively make other types use @dynamicMemberLookup
by defining it on a protocol, adding a default implementation of subscript(dynamicMember:)
using a protocol extension, then making other types conform to your protocol however you want.
For example, this creates a new Subscripting
protocol, provides a default subscript(dynamicMember:)
implementation that returns a message, then extends Swift’s String
to use that protocol:
@dynamicMemberLookup
protocol Subscripting { }
extension Subscripting {
subscript(dynamicMember member: String) -> String {
return "This is coming from the subscript"
}
}
extension String: Subscripting { }
let str = "Hello, Swift"
print(str.username)
Conditional conformances were introduced in Swift 4.1, allowing types to conform to a protocol only when certain conditions are met.
For example, if we had a Purchaseable
protocol:
protocol Purchaseable {
func buy()
}
And a simple type that conforms to that protocol:
struct Book: Purchaseable {
func buy() {
print("You bought a book")
}
}
Then we could make Array
conform to Purchaseable
if all the elements inside the array were also Purchasable
:
extension Array: Purchaseable where Element: Purchaseable {
func buy() {
for item in self {
item.buy()
}
}
}
This worked great at compile time, but there was a problem: if you needed to query a conditional conformance at runtime, your code would crash because it wasn’t supported in Swift 4.1
In Swift 4.2 that’s now fixed, so if you receive data of one type and want to check if it can be converted to a conditionally conformed protocol, it works great.
For example:
let items: Any = [Book(), Book(), Book()]
if let books = items as? Purchaseable {
books.buy()
}
In addition, support for automatic synthesis of Hashable
conformance has improved greatly in Swift 4.2. Several built-in types from the Swift standard library – including optionals, arrays, dictionaries, and ranges – now automatically conform to the Hashable
protocol when their elements conform to Hashable
.
For example:
struct User: Hashable {
var name: String
var pets: [String]
}
Swift 4.2 can automatically synthesize Hashable
conformance for that struct, but Swift 4.1 could not.
SE-0202 introduced a new random API that’s native to Swift. This means you can for the most part stop using arc4random_uniform()
and GameplayKit to get randomness, and instead rely on a cryptographically secure randomizer that’s baked right into the core of the language.
You can generate random numbers by calling the random()
method on whatever numeric type you want, providing the range you want to work with. For example, this generates a random number in the range 1 through 4, inclusive on both sides:
let randomInt = Int.random(in: 1..<5)
Similar methods exist for Float
, Double
, and CGFloat
:
let randomFloat = Float.random(in: 1..<10)
let randomDouble = Double.random(in: 1...100)
let randomCGFloat = CGFloat.random(in: 1...1000)
There’s also one for booleans, generating either true or false randomly:
let randomBool = Bool.random()
Checking a random boolean is effectively the same as checking Int.random(in: 0...1) == 1
, but it expresses your intent more clearly.
SE-0202 also includes support for shuffling arrays using new shuffle()
and shuffled()
methods depending on whether you want in-place shuffling or not. For example:
var albums = ["Red", "1989", "Reputation"]
// shuffle in place
albums.shuffle()
// get a shuffled array back
let shuffled = albums.shuffled()
It also adds a new randomElement()
method to arrays, which returns one random element from the array if it isn’t empty, or nil otherwise:
if let random = albums.randomElement() {
print("The random album is \(random).")
}
Swift 4.2 implemented SE-0206, which simplifies the way we make custom types conform to the Hashable
protocol.
From Swift 4.1 onwards conformance to Hashable
can be synthesized by the compiler. However, if you want your own hashing implementation – for example, if your type has many properties but you know that one of them was enough to identify it uniquely – you still need to write your own code using whatever algorithm you thought was best.
Swift 4.2 introduced a new Hasher
struct that provides a randomly seeded, universal hash function to make this process easier:
struct iPad: Hashable {
var serialNumber: String
var capacity: Int
func hash(into hasher: inout Hasher) {
hasher.combine(serialNumber)
}
}
You can add more properties to your hash by calling combine()
repeatedly, and the order in which you add properties affects the finished hash value.
You can also use Hasher
as a standalone hash generator: just provide it with whatever values you want to hash, then call finalize()
to generate the final value. For example:
let first = iPad(serialNumber: "12345", capacity: 256)
let second = iPad(serialNumber: "54321", capacity: 512)
var hasher = Hasher()
hasher.combine(first)
hasher.combine(second)
let hash = hasher.finalize()
Hasher
uses a random seed every time it hashes an object, which means the hash value for any object is effectively guaranteed to be different between runs of your app.
This in turn means that elements you add to a set or a dictionary are highly likely to have a different order each time you run your app.
SE-0207 provides a new allSatisfy()
method that checks whether all items in a sequence pass a condition.
For example, if we had an array of exam results like this:
let scores = [85, 88, 95, 92]
We could decide whether a student passed their course by checking whether all their exam results were 85 or higher:
let passed = scores.allSatisfy { $0 >= 85 }
SE-0197 introduced a new removeAll(where:)
method that performs a high-performance, in-place filter for collections. You give it a closure condition to run, and it will strip out all objects that match the condition.
For example, if you have a collection of names and want to remove people called “Terry”, you’d use this:
var pythons = ["John", "Michael", "Graham", "Terry", "Eric", "Terry"]
pythons.removeAll { $0.hasPrefix("Terry") }
print(pythons)
Now, you might very well think that you could accomplish that by using filter()
like this:
pythons = pythons.filter { !$0.hasPrefix("Terry") }
However, that doesn’t use memory very efficiently, it specifies what you don’t want rather than what you want, and more advanced in-place solutions come with a range of complexities that are off-putting to novices. Ben Cohen, the author of SE-0197, gave a talk at dotSwift 2018 where he discussed the implementation of this proposal in more detail – if you’re keen to learn why it’s so efficient, you should start there!
SE-0199 introduced a new toggle()
method to booleans that flip them between true and false..
The entire code to implement proposal is only a handful of lines of Swift:
extension Bool {
mutating func toggle() {
self = !self
}
}
However, the end result makes for much more natural Swift code:
var loggedIn = false
loggedIn.toggle()
As noted in the proposal, this is particularly useful in more complex data structures: myVar.prop1.prop2.enabled.toggle()
avoids the potential typing errors that could be caused using manual negation.
The proposal makes Swift easier and safer to write, and is purely additive, so I think most folks will switch to using it quickly enough.
SE-0200 added the ability to create raw strings, where backslashes and quote marks are interpreted as those literal symbols rather than escapes characters or string terminators. This makes a number of use cases more easy, but regular expressions in particular will benefit.
To use raw strings, place one or more #
symbols before your strings, like this:
let rain = #"The "rain" in "Spain" falls mainly on the Spaniards."#
The #
symbols at the start and end of the string become part of the string delimiter, so Swift understands that the standalone quote marks around “rain” and “Spain” should be treated as literal quote marks rather than ending the string.
Raw strings allow you to use backslashes too:
let keypaths = #"Swift keypaths such as \Person.name hold uninvoked references to properties."#
That treats the backslash as being a literal character in the string, rather than an escape character. This in turn means that string interpolation works differently:
let answer = 42
let dontpanic = #"The answer to life, the universe, and everything is \#(answer)."#
Notice how I’ve used \#(answer)
to use string interpolation – a regular \(answer)
will be interpreted as characters in the string, so when you want string interpolation to happen in a raw string you must add the extra #
.
One of the interesting features of Swift’s raw strings is the use of hash symbols at the start and end, because you can use more than one in the unlikely event you’ll need to. It’s hard to provide a good example here because it really ought to be extremely rare, but consider this string: My dog said "woof"#gooddog. Because there’s no space before the hash, Swift will see "#
and immediately interpret it as the string terminator. In this situation we need to change our delimiter from #"
to ##"
, like this:
let str = ##"My dog said "woof"#gooddog"##
Notice how the number of hashes at the end must match the number at the start.
Raw strings are fully compatible with Swift’s multi-line string system – just use #"""
to start, then """#
to end, like this:
let multiline = #"""
The answer to life,
the universe,
and everything is \#(answer).
"""#
Being able to do without lots of backslashes will prove particularly useful in regular expressions. For example, writing a simple regex to find keypaths such as \Person.name
used to look like this:
let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"
Thanks to raw strings we can write the same thing with half the number of backslashes:
let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#
We still need some, because regular expressions use them too.
Result
typeSE-0235 introduced a Result
type into the standard library, giving us a simpler, clearer way of handling errors in complex code such as asynchronous APIs.
Swift’s Result
type is implemented as an enum that has two cases: success
and failure
. Both are implemented using generics so they can have an associated value of your choosing, but failure
must be something that conforms to Swift’s Error
type.
To demonstrate Result
, we could write a function that connects to a server to figure out how many unread messages are waiting for the user. In this example code we’re going to have just one possible error, which is that the requested URL string isn’t a valid URL:
enum NetworkError: Error {
case badURL
}
The fetching function will accept a URL string as its first parameter, and a completion handler as its second parameter. That completion handler will itself accept a Result
, where the success case will store an integer, and the failure case will be some sort of NetworkError
. We’re not actually going to connect to a server here, but using a completion handler at least lets us simulate asynchronous code.
Here’s the code:
import Foundation
func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(.failure(.badURL))
return
}
// complicated networking code here
print("Fetching \(url.absoluteString)...")
completionHandler(.success(5))
}
To use that code we need to check the value inside our Result
to see whether our call succeeded or failed, like this:
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
switch result {
case .success(let count):
print("\(count) unread messages.")
case .failure(let error):
print(error.localizedDescription)
}
}
There are three more things you ought to know before you start using Result
in your own code.
First, Result
has a get()
method that either returns the successful value if it exists, or throws its error otherwise. This allows you to convert Result
into a regular throwing call, like this:
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
if let count = try? result.get() {
print("\(count) unread messages.")
}
}
Second, Result
has an initializer that accepts a throwing closure: if the closure returns a value successfully that gets used for the success
case, otherwise the thrown error is placed into the failure
case.
For example:
let result = Result { try String(contentsOfFile: someFile) }
Third, rather than using a specific error enum that you’ve created, you can also use the general Error
protocol. In fact, the Swift Evolution proposal says “it's expected that most uses of Result will use Swift.Error
as the Error
type argument.”
So, rather than using Result<Int, NetworkError>
you could use Result<Int, Error>
. Although this means you lose the safety of typed throws, you gain the ability to throw a variety of different error enums – which you prefer really depends on your coding style.
SE-0228 dramatically revamped Swift’s string interpolation system so that it’s more efficient and more flexible, and it’s creating a whole new range of features that were previously impossible.
In its most basic form, the new string interpolation system lets us control how objects appear in strings. Swift has default behavior for structs that is helpful for debugging, because it prints the struct name followed by all its properties. But if you were working with classes (that don’t have this behavior), or wanted to format that output so it could be user-facing, then you could use the new string interpolation system.
For example, if we had a struct like this:
struct User {
var name: String
var age: Int
}
If we wanted to add a special string interpolation for that so that we printed users neatly, we would add an extension to String.StringInterpolation
with a new appendInterpolation()
method. Swift already has several of these built in, and uses the interpolation type – in this case User
to figure out which method to call.
In this case, we’re going to add an implementation that puts the user’s name and age into a single string, then calls one of the built-in appendInterpolation()
methods to add that to our string, like this:
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: User) {
appendInterpolation("My name is \(value.name) and I'm \(value.age)")
}
}
Now we can create a user and print out their data:
let user = User(name: "Guybrush Threepwood", age: 33)
print("User details: \(user)")
That will print User details: My name is Guybrush Threepwood and I'm 33, whereas with the custom string interpolation it would have printed User details: User(name: "Guybrush Threepwood", age: 33). Of course, that functionality is no different from just implementing the CustomStringConvertible
protocol, so let’s move on to more advanced usages.
Your custom interpolation method can take as many parameters as you need, labeled or unlabeled. For example, we could add an interpolation to print numbers using various styles, like this:
extension String.StringInterpolation {
mutating func appendInterpolation(_ number: Int, style: NumberFormatter.Style) {
let formatter = NumberFormatter()
formatter.numberStyle = style
if let result = formatter.string(from: number as NSNumber) {
appendLiteral(result)
}
}
}
The NumberFormatter
class has a number of styles, including currency ($72.83), ordinal (1st, 12th), and spell out (five, forty-three). So, we could create a random number and have it spelled out into a string like this:
let number = Int.random(in: 0...100)
let lucky = "The lucky number this week is \(number, style: .spellOut)."
print(lucky)
You can call appendLiteral()
as many times as you need, or even not at all if necessary. For example, we could add a string interpolation to repeat a string multiple times, like this:
extension String.StringInterpolation {
mutating func appendInterpolation(repeat str: String, _ count: Int) {
for _ in 0 ..< count {
appendLiteral(str)
}
}
}
print("Baby shark \(repeat: "doo ", 6)")
And, as these are just regular methods, you can use Swift’s full range of functionality. For example, we might add an interpolation that joins an array of strings together, but if that array is empty execute a closure that returns a string instead:
extension String.StringInterpolation {
mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
if values.count == 0 {
appendLiteral(defaultValue())
} else {
appendLiteral(values.joined(separator: ", "))
}
}
}
let names = ["Harry", "Ron", "Hermione"]
print("List of students: \(names, empty: "No one").")
Using @autoclosure
means that we can use simple values or call complex functions for the default value, but none of that work will be done unless values.count
is zero.
With a combination of the ExpressibleByStringLiteral
and ExpressibleByStringInterpolation
protocols it’s now possible to create whole types using string interpolation, and if we add CustomStringConvertible
we can even make those types print as strings however we want.
To make this work, we need to fulfill some specific criteria:
ExpressibleByStringLiteral
, ExpressibleByStringInterpolation
, and CustomStringConvertible
. The latter is only needed if you want to customize the way the type is printed.StringInterpolation
that conforms to StringInterpolationProtocol
.appendLiteral()
method, as well as one or more appendInterpolation()
methods.We can put all that together into an example type that can construct HTML from various common elements. The “scratchpad” inside the nested StringInterpolation
struct will be a string: each time a new literal or interpolation is added, we’ll append it to the string. To help you see exactly what’s going on, I’ve added some print()
calls inside the various append methods.
Here’s the code.
struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
struct StringInterpolation: StringInterpolationProtocol {
// start with an empty string
var output = ""
// allocate enough space to hold twice the amount of literal text
init(literalCapacity: Int, interpolationCount: Int) {
output.reserveCapacity(literalCapacity * 2)
}
// a hard-coded piece of text – just add it
mutating func appendLiteral(_ literal: String) {
print("Appending \(literal)")
output.append(literal)
}
// a Twitter username – add it as a link
mutating func appendInterpolation(twitter: String) {
print("Appending \(twitter)")
output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
}
// an email address – add it using mailto
mutating func appendInterpolation(email: String) {
print("Appending \(email)")
output.append("<a href=\"mailto:\(email)\">\(email)</a>")
}
}
// the finished text for this whole component
let description: String
// create an instance from a literal string
init(stringLiteral value: String) {
description = value
}
// create an instance from an interpolated string
init(stringInterpolation: StringInterpolation) {
description = stringInterpolation.output
}
}
We can now create and use an instance of HTMLComponent
using string interpolation like this:
let text: HTMLComponent = "You should follow me on Twitter \(twitter: "twostraws"), or you can email me at \(email: "paul@hackingwithswift.com")."
print(text)
Thanks to the print()
calls that were scattered inside, you’ll see exactly how the string interpolation functionality works: you’ll see “Appending You should follow me on Twitter”, “Appending twostraws”, “Appending , or you can email me at “, “Appending paul@hackingwithswift.com”, and finally “Appending .” – each part triggers a method call, and is added to our string.
SE-0216 adds a new @dynamicCallable
attribute to Swift, which brings with it the ability to mark a type as being directly callable. It’s syntactic sugar rather than any sort of compiler magic, effectively transforming this code:
let result = random(numberOfZeroes: 3)
Into this:
let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])
@dynamicCallable
is the natural extension of Swift 4.2's @dynamicMemberLookup
, and serves the same purpose: to make it easier for Swift code to work alongside dynamic languages such as Python and JavaScript.
To add this functionality to your own types, you need to add the @dynamicCallable
attribute plus one or both of these methods:
func dynamicallyCall(withArguments args: [Int]) -> Double
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double
The first of those is used when you call the type without parameter labels (e.g. a(b, c)
), and the second is used when you do provide labels (e.g. a(b: cat, c: dog)
).
@dynamicCallable
is really flexible about which data types its methods accept and return, allowing you to benefit from all of Swift’s type safety while still having some wriggle room for advanced usage. So, for the first method (no parameter labels) you can use anything that conforms to ExpressibleByArrayLiteral
such as arrays, array slices, and sets, and for the second method (with parameter labels) you can use anything that conforms to ExpressibleByDictionaryLiteral
such as dictionaries and key value pairs.
As well as accepting a variety of inputs, you can also provide multiple overloads for a variety of outputs – one might return a string, one an integer, and so on. As long as Swift is able to resolve which one is used, you can mix and match all you want.
Let’s look at an example. First, here’s a RandomNumberGenerator
struct that generates numbers between 0 and a certain maximum, depending on what input was passed in:
struct RandomNumberGenerator {
func generate(numberOfZeroes: Int) -> Double {
let maximum = pow(10, Double(numberOfZeroes))
return Double.random(in: 0...maximum)
}
}
To switch that over to @dynamicCallable
we’d write something like this instead:
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
let numberOfZeroes = Double(args.first?.value ?? 0)
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}
That method can be called with any number of parameters, or perhaps zero, so we read the first value carefully and use nil coalescing to make sure there’s a sensible default.
We can now create an instance of RandomNumberGenerator
and call it like a function:
let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)
If you had used dynamicallyCall(withArguments:)
instead – or at the same time, because you can have them both a single type – then you’d write this:
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withArguments args: [Int]) -> Double {
let numberOfZeroes = Double(args[0])
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}
let random = RandomNumberGenerator()
let result = random(0)
There are some important rules to be aware of when using @dynamicCallable
:
withKeywordArguments:
and don’t implement withArguments:
, your type can still be called without parameter labels – you’ll just get empty strings for the keys.withKeywordArguments:
or withArguments:
are marked as throwing, calling the type will also be throwing.@dynamicCallable
to an extension, only the primary definition of a type.Perhaps more importantly, there is no support for method resolution, which means we must call the type directly (e.g. random(numberOfZeroes: 5)
) rather than calling specific methods on the type (e.g. random.generate(numberOfZeroes: 5)
). There is already some discussion on adding the latter using a method signature such as this:
func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)
If that became possible in future Swift versions it might open up some very interesting possibilities for test mocking.
In the meantime, @dynamicCallable
is not likely to be widely popular, but it is hugely important for a small number of people who want interactivity with Python, JavaScript, and other languages.
SE-0192 adds the ability to distinguish between enums that are fixed and enums that might change in the future.
One of Swift’s security features is that it requires all switch statements to be exhaustive – that they must cover all cases. While this works well from a safety perspective, it causes compatibility issues when new cases are added in the future: a system framework might send something different that you hadn’t catered for, or code you rely on might add a new case and cause your compile to break because your switch is no longer exhaustive.
With the @unknown
attribute we can now distinguish between two subtly different scenarios: “this default case should be run for all other cases because I don’t want to handle them individually,” and “I want to handle all cases individually, but if anything comes up in the future use this rather than causing an error.”
Here’s an example enum:
enum PasswordError: Error {
case short
case obvious
case simple
}
We could write code to handle each of those cases using a switch
block:
func showOld(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
default:
print("Your password was too simple.")
}
}
That uses two explicit cases for short and obvious passwords, but bundles the third case into a default block.
Now, if in the future we added a new case to the enum called old
, for passwords that had been used previously, our default
case would automatically be called even though its message doesn’t really make sense – the password might not be too simple.
Swift can’t warn us about this code because it’s technically correct (the best kind of correct), so this mistake would easily be missed. Fortunately, the new @unknown
attribute fixes it perfectly – it can be used only on the default
case, and is designed to be run when new cases come along in the future.
For example:
func showNew(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
@unknown default:
print("Your password wasn't suitable.")
}
}
That code will now issue warnings because the switch
block is no longer exhaustive – Swift wants us to handle each case explicitly. Helpfully this is only a warning, which is what makes this attribute so useful: if a framework adds a new case in the future you’ll be warned about it, but it won’t break your source code.
try?
SE-0230 modifies the way try?
works so that nested optionals are flattened to become regular optionals. This makes it work the same way as optional chaining and conditional typecasts, both of which flatten optionals in earlier Swift versions.
Here’s a practical example that demonstrates the change:
struct User {
var id: Int
init?(id: Int) {
if id < 1 {
return nil
}
self.id = id
}
func getMessages() throws -> String {
// complicated code here
return "No messages"
}
}
let user = User(id: 1)
let messages = try? user?.getMessages()
The User
struct has a failable initializer, because we want to make sure folks create users with a valid ID. The getMessages()
method would in theory contain some sort of complicated code to get a list of all the messages for the user, so it’s marked as throws
; I’ve made it return a fixed string so the code compiles.
The key line is the last one: because the user is optional it uses optional chaining, and because getMessages()
can throw it uses try?
to convert the throwing method into an optional, so we end up with a nested optional. In Swift 4.2 and earlier this would make messages
a String??
– an optional optional string – but in Swift 5.0 and later try?
won’t wrap values in an optional if they are already optional, so messages
will just be a String?
.
This new behavior matches the existing behavior of optional chaining and conditional typecasting. That is, you could use optional chaining a dozen times in a single line of code if you wanted, but you wouldn’t end up with 12 nested optionals. Similarly, if you used optional chaining with as?
, you would still end up with only one level of optionality, because that’s usually what you want.
SE-0225 adds an isMultiple(of:)
method to integers, allowing us to check whether one number is a multiple of another in a much clearer way than using the division remainder operation, %
.
For example:
let rowNumber = 4
if rowNumber.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}
Yes, we could write the same check using if rowNumber % 2 == 0
but you have to admit that’s less clear – having isMultiple(of:)
as a method means it can be listed in code completion options in Xcode, which aids discoverability.
compactMapValues()
SE-0218 adds a new compactMapValues()
method to dictionaries, bringing together the compactMap()
functionality from arrays (“transform my values, unwrap the results, then discard anything that’s nil”) with the mapValues()
method from dictionaries (“leave my keys intact but transform my values”).
As an example, here’s a dictionary of people in a race, along with the times they took to finish in seconds. One person did not finish, marked as “DNF”:
let times = [
"Hudson": "38",
"Clarke": "42",
"Robinson": "35",
"Hartis": "DNF"
]
We can use compactMapValues()
to create a new dictionary with names and times as an integer, with the one DNF person removed:
let finishers1 = times.compactMapValues { Int($0) }
Alternatively, you could just pass the Int
initializer directly to compactMapValues()
, like this:
let finishers2 = times.compactMapValues(Int.init)
You can also use compactMapValues()
to unwrap optionals and discard nil values without performing any sort of transformation, like this:
let people = [
"Paul": 38,
"Sophie": 8,
"Charlotte": 5,
"William": nil
]
let knownAges = people.compactMapValues { $0 }
SE-0242 introduced major improvements to one of Swift’s most commonly used features: memberwise initializers for structs.
In earlier versions of Swift, a memberwise initializer was automatically created to accept parameters matching the properties of a struct, like this:
struct User {
var name: String
var loginCount: Int = 0
}
let piper = User(name: "Piper Chapman", loginCount: 0)
In Swift 5.1 this has been enhanced so that the memberwise initializer now uses default parameter values for any properties that have them. In the User
struct we’ve given loginCount
a default value of 0, which means we can either specify it or leave it to the memberwise initializer:
let gloria = User(name: "Gloria Mendoza", loginCount: 0)
let suzanne = User(name: "Suzanne Warren")
This lets us avoid repeating code, which is always welcome.
SE-0255 has removed a small but important inconsistency in the language: single-expression functions that return a value can now remove the return
keyword and Swift will understand it implicitly.
In previous versions of Swift, single-line closures that returned a value you could omit the return
keyword because the only line of code that was there must be the one that returned a value. So, these two pieces of code were identical:
let doubled1 = [1, 2, 3].map { $0 * 2 }
let doubled2 = [1, 2, 3].map { return $0 * 2 }
In Swift 5.1, this behavior has now been extended to functions as well: if they contain a single expression – effectively a single piece of code that evaluates to a value – then you can leave off the return
keyword, like this:
func double(_ number: Int) -> Int {
number * 2
}
That will probably cause some people to do a double take at first, but I’m sure it will become second nature over time.
Self
SE-0068 expands Swift’s use of Self
so that it refers to the containing type when used inside classes, structs, and enums. This is particularly useful for dynamic types, where the exact type of something needs to be determined at runtime.
As an example, consider this code:
class NetworkManager {
class var maximumActiveRequests: Int {
return 4
}
func printDebugData() {
print("Maximum network requests: \(NetworkManager.maximumActiveRequests).")
}
}
That declares a static maximumActiveRequests
property for a network manager, and adds a printDebugData()
method to print the static property. That works fine right now, but when NetworkManager
is subclassed things become more complicated:
class ThrottledNetworkManager: NetworkManager {
override class var maximumActiveRequests: Int {
return 1
}
}
That subclass changes maximumActiveRequests
so that it allows only one request at a time, but if we call printDebugData()
it will print out the value from its parent class:
let manager = ThrottledNetworkManager()
manager.printDebugData()
That should print out 1 rather than 4, and that’s where SE-0068 comes in: we can now write Self
(with a capital S) to refer to the current type. So, we can rewrite printDebugData()
to this:
class ImprovedNetworkManager {
class var maximumActiveRequests: Int {
return 4
}
func printDebugData() {
print("Maximum network requests: \(Self.maximumActiveRequests).")
}
}
This means Self
works the same way as it did for protocols in earlier Swift versions.
SE-0244 introduced the concept of opaque types into Swift. An opaque type is one where we’re told about the capabilities of an object without knowing specifically what kind of object it is.
At first glance that sounds a lot like a protocol, but opaque return types take the concept of protocols significantly further because are able to work with associated types, they require the same type to be used internally each time, and they allow us to hide implementation details.
As an example, if we wanted to launch different kinds of fighters from a Rebel base we might write code like this:
protocol Fighter { }
struct XWing: Fighter { }
func launchFighter() -> Fighter {
return XWing()
}
let red5 = launchFighter()
Whoever calls that function knows it will return some sort of Fighter
but doesn’t know precisely what. As a result, we could add struct YWing: Fighter { }
or other types, and have any of them be returned.
But there’s a problem: what if we wanted to check whether a specific fighter was Red 5? You might think the solution is to make Fighter
conform to the Equatable
protocol so we can use ==
. However, as soon as you do that Swift will throw up a particularly dreaded error for the launchFighter
function: “Protocol 'Fighter' can only be used as a generic constraint because it has Self or associated type requirements.”
The “Self” part of that error is what is hitting us here. The Equatable
protocol has to compare two instances of itself (“Self”) to see whether they are the same, but Swift has no guarantee that the two equatable things are remotely the same – we could be comparing a Fighter with an array of integers, for example.
Opaque types solve this problem because even though we just see a protocol being used, internally the Swift compiler knows exactly what that protocol actually resolves to – it knows it’s an XWing
, an array of strings, or whatever.
To send back an opaque type, use the keyword some
before your protocol name:
func launchOpaqueFighter() -> some Fighter {
return XWing()
}
From the caller’s perspective that still gets back a Fighter
, which might be an XWing
, a YWing
, or something else that conforms to the Fighter
protocol. But from the compiler’s perspective it knows exactly what is being returned, so it can make sure we follow all the rules correctly.
For example, consider a function that returned some Equatable
like this:
func makeInt() -> some Equatable {
Int.random(in: 1...10)
}
When we call that, all we know is that it is some sort of Equatable
value, however if call it twice then we can compare the results of those two calls because Swift knows for sure it will be the same underlying type:
let int1 = makeInt()
let int2 = makeInt()
print(int1 == int2)
The same is not true if we had a second function that returned some Equatable
, like this:
func makeString() -> some Equatable {
"Red"
}
Even though from our perspective both send us back an Equatable
type, and we can compare the results of two calls to makeString()
or two calls to makeInt()
, Swift won’t let us compare the return value of makeString()
to the return value of makeInt()
because it knows comparing a string and an integer doesn’t make any sense.
An important proviso here is that functions with opaque return types must always return one specific type. If for example we tried to use Bool.random()
to randomly launch an XWing
or a YWing
then Swift would refuse to build our code because the compiler can no longer tell what will be sent back.
You might well think “if we always need to return the same type, why not just write the function as func launchFighter() -> XWing
? While that might work sometimes, it creates new problems such as:
someArray.lazy.drop { … }
we get sent back a LazyDropWhileSequence
– a dedicated and highly specific type from the Swift standard library. All we actually care about is that this thing is a sequence; we don’t need to know how Swift’s internals work.launchFighter()
return only an XWing
means we can’t switch to a different type in the future, and given how much Disney relies on Star Wars toy sales that would be a problem! By returning an opaque type we can return X-Wings today, then move to B-Wings in a year – we only ever return one in any given build of our code, but we can still have the flexibility to change our mind.In some respects all this might sound similar to generics, which also solve the “Self or associated type requirements” problem. Generics allow us to write code like this:
protocol ImperialFighter {
init()
}
struct TIEFighter: ImperialFighter { }
struct TIEAdvanced: ImperialFighter { }
func launchImperialFighter<T: ImperialFighter>() -> T {
return T()
}
That defines a new protocol that requires conforming types to be initializable with no parameters, defines two structs that conform to that protocol, then creates a generic function to use it. However, the difference here is that now callers of launchImperialFighter()
are the ones to choose what kind of fighter they get, like this:
let fighter1: TIEFighter = launchImperialFighter()
let fighter2: TIEAdvanced = launchImperialFighter()
If you want callers to be able to select their data type then generics work well, but if you want the function to decide the return type then they fall down;
So, opaque result types allow us to do several things:
If you ever forget the difference between protocols and opaque types, think of this: returning Fighter
means "any sort of Fighter
type but we don't know what", whereas returning some Fighter
means "a specific sort of Fighter
type but we still don't know what." In the latter case, the difference is that the underlying type is something specific that the compiler knows about, whereas in the former case it can literally be anything that conforms to the protocol – even being different every time we call the method.
SE-0254 adds the ability to mark subscripts as being static, which means they apply to types rather than instances of a type.
Static properties and methods are used when one set of values is shared between all instances of that type. For example, if you had one centralized type to store your app settings, you might write code like this:
public enum OldSettings {
private static var values = [String: String]()
static func get(_ name: String) -> String? {
return values[name]
}
static func set(_ name: String, to newValue: String?) {
print("Adjusting \(name) to \(newValue ?? "nil")")
values[name] = newValue
}
}
OldSettings.set("Captain", to: "Gary")
OldSettings.set("Friend", to: "Mooncake")
print(OldSettings.get("Captain") ?? "Unknown")
Wrapping the dictionary inside a type means that we can control access more carefully, and using an enum with no cases means we can’t try to instantiate the type – we can’t make various instances of Settings
.
With Swift 5.1 we can now use a static subscript instead, allowing us to rewrite our code to this:
public enum NewSettings {
private static var values = [String: String]()
public static subscript(_ name: String) -> String? {
get {
return values[name]
}
set {
print("Adjusting \(name) to \(newValue ?? "nil")")
values[name] = newValue
}
}
}
NewSettings["Captain"] = "Gary"
NewSettings["Friend"] = "Mooncake"
print(NewSettings["Captain"] ?? "Unknown")
Custom subscripts like this have always been possible for instances of types; this improvement makes static or class subscripts possible too.
none
casesSwift’s optionals are implemented as an enum of two cases: some
and none
. This gave rise to the possibility of confusion if we created our own enums that had a none
case, then wrapped that inside an optional.
For example:
enum BorderStyle {
case none
case solid(thickness: Int)
}
Used as a non-optional this was always clear:
let border1: BorderStyle = .none
print(border1)
That will print “none”. But if we used an optional for that enum – if we didn’t know what border style to use – then we’d hit problems:
let border2: BorderStyle? = .none
print(border2)
That prints “nil”, because Swift assumes .none
means the optional is empty, rather than an optional with the value BorderStyle.none
.
In Swift 5.1 this confusion now prints a warning: “Assuming you mean 'Optional
Swift has always been smart enough to handle switch/case pattern matching between optionals and non-optionals for strings and integers, but before Swift 5.1 that wasn’t extended to enums.
Well, in Swift 5.1 we can now use switch/case pattern matching to match optional enums with non-optionals, like this:
enum BuildStatus {
case starting
case inProgress
case complete
}
let status: BuildStatus? = .inProgress
switch status {
case .inProgress:
print("Build is starting…")
case .complete:
print("Build is complete!")
default:
print("Some other build status")
}
Swift is able to compare the optional enum directly with the non-optional cases, so that code will print “Build is starting…”
SE-0240 introduced the ability to calculate and apply the differences between ordered collections. This could prove particularly interesting for developers who have complex collections in table views, where they want to add and remove lots of items smoothly using animations.
The basic principle is straightforward: Swift 5.1 gives us a new difference(from:)
method that calculates the differences between two ordered collections – what items to remove and what items to insert. This can be used with any ordered collection that contains Equatable
elements.
To demonstrate this, we can create an array of scores, calculate the difference from one to the other, then loop over those differences and apply each one to make our two collections the same.
Note: Because Swift now ships inside Apple’s operating systems, new features like this one must be used with an #available
check to make sure the code is being run on an OS that includes the new functionality. For features that will land in an unknown, unannounced operating system shipping at some point in the future, a special version number of “9999” is used to mean “we don’t know what the actual number is just yet.”
Here’s the code:
var scores1 = [100, 91, 95, 98, 100]
let scores2 = [100, 98, 95, 91, 100]
if #available(iOS 9999, *) {
let diff = scores2.difference(from: scores1)
for change in diff {
switch change {
case .remove(let offset, _, _):
scores1.remove(at: offset)
case .insert(let offset, let element, _):
scores1.insert(element, at: offset)
}
}
print(scores1)
}
For more advanced animations, you can use the third value of the changes: associatedWith
. So, rather than using .insert(let offset, let element, _)
above you might write .insert(let offset, let element, let associatedWith)
instead. This lets you track pairs of changes at the same time: moving an item two places down in your collection is a removal then an insertion, but the associatedWith
value ties those two changes together so you treat it as a move instead.
Rather than applying changes by hand, you can apply the whole collection using a new applying()
method, like this:
if #available(iOS 9999, *) {
let diff = scores2.difference(from: scores1)
let result = scores1.applying(diff) ?? []
}
SE-0245 introduced a new initializer for arrays that doesn’t pre-fill values with a default. This was previously available as a private API, which meant Xcode wouldn’t list it in its code completion but you could still use it if you wanted – and if you were happy to take the risk that it wouldn’t be withdrawn in the future!
To use the initializer, tell it the capacity you want, then provide a closure to fill in the values however you need. Your closure will be given an unsafe mutable buffer pointer where you can write your values, as well as an inout
second parameter that lets you report back how many values you actually used.
For example, we could make an array of 10 random integers like this:
let randomNumbers = Array<Int>(unsafeUninitializedCapacity: 10) { buffer, initializedCount in
for x in 0..<10 {
buffer[x] = Int.random(in: 0...10)
}
initializedCount = 10
}
There are some rules here:
initializedCount
to 0 through 10, but not 11.initializedCount
to 5 but don’t actually provide values for elements 0 through 4 – then they are likely to be filled with random data. This is A Bad Idea.initializedCount
it will be 0, so any data you assigned will be lost.Now, we could have rewritten the above code using map()
, like this:
let randomNumbers2 = (0...9).map { _ in Int.random(in: 0...10) }
That’s certainly easier to read, but it’s less efficient: it creates a range, creates a new empty array, sizes it up to the correct amount, loops over the range, and calls the closure once for each range item.
SE-0249 introduced a marvelous shortcut that allows us to use keypaths in a handful of specific circumstances.
The Evolution proposal describes this as being able to use “\Root.value
wherever functions of (Root) -> Value
are allowed”, but what it means is that if previously you sent a Car into a method and got back its license plate, you can now use Car.licensePlate
instead.
This is best understood as an example, so here’s a User
type that defines four properties:
struct User {
let name: String
let age: Int
let bestFriend: String?
var canVote: Bool {
age >= 18
}
}
We could create some instance of that struct and put them into an array, like this:
let eric = User(name: "Eric Effiong", age: 18, bestFriend: "Otis Milburn")
let maeve = User(name: "Maeve Wiley", age: 19, bestFriend: nil)
let otis = User(name: "Otis Milburn", age: 17, bestFriend: "Eric Effiong")
let users = [eric, maeve, otis]
Now for the important part: if you want to get an array of all the users names, you can do so by using a key path like this:
let userNames = users.map(\.name)
print(userNames)
Previously you would have had to write a closure to retrieve the name by hand, like this:
let oldUserNames = users.map { $0.name }
This same approach works elsewhere – anywhere where previously you would have received a value and passed back one of its properties, you can now use a key path instead. For example, this will return all users who can vote:
let voters = users.filter(\.canVote)
And this will return the best friends for all users who have one:
let bestFriends = users.compactMap(\.bestFriend)
SE-0253 introduced statically callable values to Swift, which is a fancy way of saying that you can now call a value directly if its type implements a method named callAsFunction()
. You don’t need to conform to any special protocol to make this behavior work; you just need to add that method to your type.
For example, we could create a Dice
struct that has properties for lowerBound
and upperBound
, then add callAsFunction
so that every time you call a dice value you get a random roll:
struct Dice {
var lowerBound: Int
var upperBound: Int
func callAsFunction() -> Int {
(lowerBound...upperBound).randomElement()!
}
}
let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6()
print(roll1)
That will print a random number from 1 through 6, and it’s identical to just using callAsFunction()
directly. For example, we could call it like this:
let d12 = Dice(lowerBound: 1, upperBound: 12)
let roll2 = d12.callAsFunction()
print(roll2)
Swift automatically adapts your call sites based on how callAsFunction()
is defined. For example, you can add as many parameters as you want, you can control the return value, and you can even mark methods as mutating
if needed.
For example, this creates a StepCounter
struct that tracks how far someone has walked and reports back whether they reached their target of 10,000 steps:
struct StepCounter {
var steps = 0
mutating func callAsFunction(count: Int) -> Bool {
steps += count
print(steps)
return steps > 10_000
}
}
var steps = StepCounter()
let targetReached = steps(count: 10)
For more advanced usage, callAsFunction()
supports both throws
and rethrows
, and you can even define multiple callAsFunction()
methods on a single type – Swift will choose the correct one depending on the call site, just like regular overloading.
When adding custom subscripts to a type, you can now use default arguments for any of the parameters. For example, if we had a PoliceForce
struct with a custom subscript to read officers from the force, we could add a default
parameter to send back if someone tries to read an index outside of the array’s bounds:
struct PoliceForce {
var officers: [String]
subscript(index: Int, default default: String = "Unknown") -> String {
if index >= 0 && index < officers.count {
return officers[index]
} else {
return `default`
}
}
}
let force = PoliceForce(officers: ["Amy", "Jake", "Rosa", "Terry"])
print(force[0])
print(force[5])
That will print “Amy” then “Unknown”, with the latter being caused because there is no officer at index 5. Note that you do need to write your parameter labels twice if you want them to be used, because subscripts don’t use parameter labels otherwise.
So, because I use default default
in my subscript, I can use a custom value like this:
print(force[-1, default: "The Vulture"])
There’s a small change in Swift 5.2 that could potentially cause your functionality to break: if you use a lazy sequence such as an array, and apply multiple filters to it, those filters are now run in the reverse order.
For example, this code below has one filter that selects names that start with S, then a second filter that prints out the name then returns true:
let people = ["Arya", "Cersei", "Samwell", "Stannis"]
.lazy
.filter { $0.hasPrefix("S") }
.filter { print($0); return true }
_ = people.count
In Swift 5.2 and later that will print “Samwell” and “Stannis”, because after the first filter runs those are the only names that remain to go into the second filter. But before Swift 5.2 it would have returned all four names, because the second filter would have been run before the first one. This was confusing, because if you removed the lazy
then the code would always return just Samwell and Stannis, regardless of Swift version.
This is particularly problematic because the behavior of this depends on where the code is being run: if you run Swift 5.2 code on iOS 13.3 or earlier, or macOS 10.15.3 or earlier, then you’ll get the old backward behavior, but the same code running on newer operating systems will give the new, correct behavior.
Swift 5.2 introduced a new diagnostic architecture that aims to improves the quality and precision of error messages issued by Xcode when you make a coding error. This is particularly apparent when working with SwiftUI code, where Swift would often produce false positive error messages.
For an example, consider code like this:
struct ContentView: View {
@State private var name = 0
var body: some View {
VStack {
Text("What is your name?")
TextField("Name", text: $name)
.frame(maxWidth: 300)
}
}
}
That attempts to bind a TextField
view to an integer @State
property, which is invalid. In Swift 5.1 this caused an error for the frame()
modifier saying 'Int' is not convertible to 'CGFloat?’, but in Swift 5.2 and later this correctly identifies the error is the $name
binding: Cannot convert value of type Binding<Int>
to expected argument type Binding<String>
.
You can find out more about the new diagnostic architecture on the Swift.org blog.
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.
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 introduced 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)
.
SE-0287 improves Swift’s ability to use implicit member expressions, so rather than just having support for exactly one single static member you can make chains of them.
Swift has always had the ability to use implicit member syntax for simple expressions, for example if you wanted to color some text in SwiftUI you could use .red
rather than Color.red
:
import SwiftUI
struct ContentView1: View {
var body: some View {
Text("You're not my supervisor!")
.foregroundColor(.red)
}
}
Prior to Swift 5.4 this did not work with more complex expressions. For example, if you wanted your red color to be slightly transparent you would need to write this:
struct ContentView2: View {
var body: some View {
Text("You're not my supervisor!")
.foregroundColor(Color.red.opacity(0.5))
}
}
From Swift 5.4 onwards the compiler is able to understand multiple chained members, meaning that the Color
type can be inferred:
struct ContentView3: View {
var body: some View {
Text("You're not my supervisor!")
.foregroundColor(.red.opacity(0.5))
}
}
SE-0284 introduced the ability to have functions, subscripts, and initializers use multiple variadic parameters as long as all parameters that follow a variadic parameter have labels. Before Swift 5.4, you could only have one variadic parameter in this situation.
So, with this improvement in place we could write a function that accepts a variadic parameter storing the times goals were scored during a football match, plus a second variadic parameter scoring the names of players who scored:
import Foundation
func summarizeGoals(times: Int..., players: String...) {
let joinedNames = ListFormatter.localizedString(byJoining: players)
let joinedTimes = ListFormatter.localizedString(byJoining: times.map(String.init))
print("\(times.count) goals where scored by \(joinedNames) at the follow minutes: \(joinedTimes)")
}
To call that function, provide both sets of values as variadic parameters, making sure that all parameters after the first variadic are labeled:
summarizeGoals(times: 18, 33, 55, 90, players: "Dani", "Jamie", "Roy")
SR-10069 requested the ability to overload functions in local contexts, which in practice means nested functions can now be overloaded so that Swift chooses which one to run based on the types that are used.
For example, if we wanted to make some simple cookies we might write code like this:
struct Butter { }
struct Flour { }
struct Sugar { }
func makeCookies() {
func add(item: Butter) {
print("Adding butter…")
}
func add(item: Flour) {
print("Adding flour…")
}
func add(item: Sugar) {
print("Adding sugar…")
}
add(item: Butter())
add(item: Flour())
add(item: Sugar())
print("Come and get some cookies!")
}
makeCookies()
Prior to Swift 5.4, the three add()
methods could be overloaded only if they were not nested inside makeCookies()
, but from Swift 5.4 onwards function overloading is supported in this case as well.
Swift 5.4 also lets us call local functions before they are declared, meaning that we can now write code like this if needed:
func makeCookies2() {
add(item: Butter())
add(item: Flour())
add(item: Sugar())
func add(item: Butter) {
print("Adding butter…")
}
func add(item: Flour) {
print("Adding flour…")
}
func add(item: Sugar) {
print("Adding sugar…")
}
}
makeCookies2()
From Swift 5.4 onwards it’s possible to create a local variable by calling a function of the same name. That might sound obscure, but it’s actually a problem we hit all the time.
For example, this creates a struct with a color(forRow:)
method, which gets called and assigned to a local variable called color
:
struct Table {
let count = 10
func color(forRow row: Int) -> String {
if row.isMultiple(of: 2) {
return "red"
} else {
return "black"
}
}
func printRows() {
for i in 0..<count {
let color = color(forRow: i)
print("Row \(i): \(color)")
}
}
}
let table = Table()
table.printRows()
That kind of usage is only allowed from Swift 5.4 and later. In earlier versions of Swift, it would create a circular reference because Swift couldn’t distinguish between the local color
constant and the color(forRow:)
method it was calling – you would have seen the error “Variable used within its own initial value”.
This usually resulted in us either using self.color(forRow: 1989)
to make it clear we mean the method call, or just naming the local value something else such as colorForRow
.
Fortunately Swift 5.4 resolves this and allows us to use the more natural naming.
This change also allows us to make local copies of properties and global variables. For example, we can take a copy of a username
property that is also called username
, like this:
struct User {
let username = "Taylor"
func suggestAlternativeUsername() -> String {
var username = username
username += String(Int.random(in: 1000...9999))
return username
}
}
let user = User()
user.suggestAlternativeUsername()
Because this also applies to global variables, that same code works just fine even without the struct in place:
let username = "Taytay"
func suggestAlternativeUsername() -> String {
var username = username
username += String(Int.random(in: 1000...9999))
return username
}
suggestAlternativeUsername()
Function builders unofficially arrived in Swift 5.1, but in the run up to Swift 5.4 they formally went through the Swift Evolution proposal process as SE-0289 in order to be discussed and refined. As part of that process they were renamed to result builders to better reflect their actual purpose, and even acquired some new functionality.
First up, the most important part: result builders allow us to create a new value step by step by passing in a sequence of our choosing. They power large parts of SwiftUI’s view creation system, so that when we have a VStack
with a variety of views inside, Swift silently groups them together into an internal TupleView
type so that they can be stored as a single child of the VStack
– it turns a sequence of views into a single view.
Result builders deserve their own detailed article, but I at least want to give you some small code examples so you can see them in action.
Here is a function that returns a single string:
func makeSentence1() -> String {
"Why settle for a Duke when you can have a Prince?"
}
print(makeSentence1())
That works great, but what if had several strings we wanted to join together? Just like SwiftUI, we might want to provide them all individually and have Swift figure it out, however this kind of code won’t work:
// This is invalid Swift, and will not compile.
// func makeSentence2() -> String {
// "Why settle for a Duke"
// "when you can have"
// "a Prince?"
// }
By itself, that code won’t work because Swift no longer understands what we mean. However, we could create a result builder that understands how to convert several strings into one string using whatever transformation we want, like this:
@resultBuilder
struct SimpleStringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}
}
Even though that’s a small amount of code, there’s a lot to unpack:
@resultBuilder
attribute tells Swift the following type should be treated as a result builder. Previously this behavior was achieved using @_functionBuilder
, which had an underscore to show that this wasn’t designed for general use.buildBlock()
, which should take in some sort of data and transform it. The example above takes in zero or more strings, joins them, and sends them back as a single string.SimpleStringBuilder
struct becomes a result builder, meaning that we can use @SimpleStringBuilder
anywhere we need its string joining powers.There’s nothing to stop us from using SimpleStringBuilder.buildBlock()
directly, like this:
let joined = SimpleStringBuilder.buildBlock(
"Why settle for a Duke",
"when you can have",
"a Prince?"
)
print(joined)
However, because we used the @resultBuilder
annotation with our SimpleStringBuilder
struct, we can also apply that to functions, like this:
@SimpleStringBuilder func makeSentence3() -> String {
"Why settle for a Duke"
"when you can have"
"a Prince?"
}
print(makeSentence3())
Notice how we no longer need the commas at the end of each string – @resultBuilder
automatically transforms each statement in makeSentence()
into a single string by using SimpleStringBuilder
.
In practice, result builders are capable of significantly more, accomplished by adding more methods to your builder type. For example, we could add if/else support to our SimpleStringBuilder
by adding two extra methods that describe how we want to transform the data. In our code we don’t want to transform our strings at all, so we can send them right back:
@resultBuilder
struct ConditionalStringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}
static func buildEither(first component: String) -> String {
return component
}
static func buildEither(second component: String) -> String {
return component
}
}
I know that looks like we’ve done almost no work, but now our functions are able to use conditions:
@ConditionalStringBuilder func makeSentence4() -> String {
"Why settle for a Duke"
"when you can have"
if Bool.random() {
"a Prince?"
} else {
"a King?"
}
}
print(makeSentence4())
Similarly, we could add support for loops by adding a buildArray()
method to our builder type:
@resultBuilder
struct ComplexStringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}
static func buildEither(first component: String) -> String {
return component
}
static func buildEither(second component: String) -> String {
return component
}
static func buildArray(_ components: [String]) -> String {
components.joined(separator: "\n")
}
}
And now we can use for
loops:
@ComplexStringBuilder func countDown() -> String {
for i in (0...10).reversed() {
"\(i)…"
}
"Lift off!"
}
print(countDown())
It feels almost like magic because the result builder system is doing almost all the work for us, and even though our example has been fairly simple I hope you can get a taste for the remarkable power result builders bring to Swift.
It’s worth adding that Swift 5.4 extends the result builder system to support attributes being placed on stored properties, which automatically adjusts the implicit memberwise initializer for structs to apply the result builder.
This is particularly helpful for custom SwiftUI views that use result builders, such as this one:
import SwiftUI
struct CustomVStack<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack {
// custom functionality here
content
}
}
}
If you’d like to see more advanced, real-world examples of result builders in action, you should check out the Awesome Function Builders repository on GitHub.
Property wrappers were first introduced in Swift 5.1 as a way of attaching extra functionality to properties in an easy, reusable way, but in Swift 5.4 their behavior got extended to support using them as local variables in functions.
For example, we could create a property wrapper that ensures its value never goes below zero:
@propertyWrapper struct NonNegative<T: Numeric & Comparable> {
var value: T
var wrappedValue: T {
get { value }
set {
if newValue < 0 {
value = 0
} else {
value = newValue
}
}
}
init(wrappedValue: T) {
if wrappedValue < 0 {
self.value = 0
} else {
self.value = wrappedValue
}
}
}
And from Swift 5.4 onwards we can use that property wrapper inside a regular function, rather than just attaching to a property. For example, we might write a game where our player can gain or lose points, but their score should never go below 0:
func playGame() {
@NonNegative var score = 0
// player was correct
score += 4
// player was correct again
score += 8
// player got one wrong
score -= 15
// player got another one wrong
score -= 16
print(score)
}
playGame()
SE-0294 adds a new target option for apps using Swift Package manager, allowing us to explicitly declare an executable target.
This is particularly important for folks who want to use SE-0281 (using @main
to mark your program’s entry point), because it didn’t play nicely with Swift Package Manager – it would always look for a main.swift file.
With this change, we can now remove main.swift and use @main
instead. Note: You must specify // swift-tools-version:5.4
in your Package.swift file in order to get this new functionality.
SE-0296 introduced asynchronous (async) functions into Swift, allowing us to run complex asynchronous code almost is if it were synchronous. This is done in two steps: marking async functions with the new async
keyword, then calling them using the await
keyword, similar to other languages such as C# and JavaScript.
To see how async/await helps the language, it’s helpful to look at how we solved the same problem previously. Completion handlers are commonly used in Swift code to allow us to send back values after a function returns, but they had tricky syntax as you’ll see.
For example, if we wanted to write code that fetched 100,000 weather records from a server, processes them to calculate the average temperature over time, then uploaded the resulting average back to a server, we might have written this:
import Foundation
func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) {
// Complex networking code here; we'll just send back 100,000 random temperatures
DispatchQueue.global().async {
let results = (1...100_000).map { _ in Double.random(in: -10...30) }
completion(results)
}
}
func calculateAverageTemperature(for records: [Double], completion: @escaping (Double) -> Void) {
// Sum our array then divide by the array size
DispatchQueue.global().async {
let total = records.reduce(0, +)
let average = total / Double(records.count)
completion(average)
}
}
func upload(result: Double, completion: @escaping (String) -> Void) {
// More complex networking code; we'll just send back "OK"
DispatchQueue.global().async {
completion("OK")
}
}
I’ve substituted actual networking code with fake values because the networking part isn’t relevant here. What matters is that each of those functions can take some time to run, so rather than blocking execution of the function and returning a value directly we instead use a completion closure to send something back only when we’re ready.
When it comes to using that code, we need to call them one by one in a chain, providing completion closures for each one to continue the chain, like this:
fetchWeatherHistory { records in
calculateAverageTemperature(for: records) { average in
upload(result: average) { response in
print("Server response: \(response)")
}
}
}
Hopefully you can see the problems with this approach:
@escaping (String) -> Void
can be hard to read.Result
type, it was harder to send back errors with completion handlers.From Swift 5.5, we can now clean up our functions by marking them as asynchronously returning a value rather than relying on completion handlers, like this:
func fetchWeatherHistory() async -> [Double] {
(1...100_000).map { _ in Double.random(in: -10...30) }
}
func calculateAverageTemperature(for records: [Double]) async -> Double {
let total = records.reduce(0, +)
let average = total / Double(records.count)
return average
}
func upload(result: Double) async -> String {
"OK"
}
That has already removed a lot of the syntax around returning values asynchronously, but at the call site it’s even cleaner:
func processWeather() async {
let records = await fetchWeatherHistory()
let average = await calculateAverageTemperature(for: records)
let response = await upload(result: average)
print("Server response: \(response)")
}
As you can see, all the closures and indenting have gone, making for what is sometimes called “straight-line code” – apart from the await
keywords, it looks just like synchronous code.
There are some straightforward, specific rules about the way async functions work:
That last point is important, because it allows library authors to provide both synchronous and asynchronous versions of their code without having to name the async functions specially.
The addition of async
/await
fits perfectly alongside try
/catch
, meaning that async functions and initializers can throw errors if needed. The only proviso here is that Swift enforces a particular order for the keywords, and that order is reversed between call site and function.
For example, we might have functions that attempt to fetch a number of users from a server, and save them to disk, both of which might fail by throwing errors:
enum UserError: Error {
case invalidCount, dataTooLong
}
func fetchUsers(count: Int) async throws -> [String] {
if count > 3 {
// Don't attempt to fetch too many users
throw UserError.invalidCount
}
// Complex networking code here; we'll just send back up to `count` users
return Array(["Antoni", "Karamo", "Tan"].prefix(count))
}
func save(users: [String]) async throws -> String {
let savedUsers = users.joined(separator: ",")
if savedUsers.count > 32 {
throw UserError.dataTooLong
} else {
// Actual saving code would go here
return "Saved \(savedUsers)!"
}
}
As you can see, both those functions are marked async throws
– they are asynchronous functions, and they might throw errors.
When it comes to calling them the order of keywords is flipped to try await
rather than await try
, like this:
func updateUsers() async {
do {
let users = try await fetchUsers(count: 3)
let result = try await save(users: users)
print(result)
} catch {
print("Oops!")
}
}
So, “asynchronous, throwing” in the function definition, but “throwing, asynchronous” at the call site – think of it as unwinding a stack. Not only does try await
read a little more naturally than await try
, but it’s also more reflective of what’s actually happening: we’re waiting for some work to complete, and when it does complete it might end up throwing.
With async/await now in Swift itself, the Result
type introduced in Swift 5.0 becomes much less important as one of its primary benefits was improving completion handlers. That doesn’t mean Result
is useless, because it’s still the best way to store the result of an operation for later evaluation.
Important: Making a function asynchronous doesn’t mean it magically runs concurrently with other code, which means unless you specify otherwise calling multiple async functions will still run them sequentially.
All the async
functions you’ve seen so far have in turn been called by other async
functions, which is intentional: taken by itself this Swift Evolution proposal does not actually provide any way to run asynchronous code from a synchronous context. Instead, this functionality is defined in a separate Structured Concurrency proposal, although hopefully we’ll see some major updates to Foundation too.
SE-0298 introduced the ability to loop over asynchronous sequences of values using a new AsyncSequence
protocol. This is helpful for places when you want to process values in a sequence as they become available rather than precomputing them all at once – perhaps because they take time to calculate, or because they aren’t available yet.
Using AsyncSequence
is almost identical to using Sequence
, with the exception that your types should conform to AsyncSequence
and AsyncIterator
, and your next()
method should be marked async
. When it comes time for your sequence to end, make sure you send back nil
from next()
, just as with Sequence
.
For example, we could make a DoubleGenerator
sequence that starts from 1 and doubles its number every time it’s called:
struct DoubleGenerator: AsyncSequence {
typealias Element = Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 1
mutating func next() async -> Int? {
defer { current &*= 2 }
if current < 0 {
return nil
} else {
return current
}
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator()
}
}
Tip: If you just remove “async” from everywhere it appears in that code, you have a valid Sequence
doing exactly the same thing – that’s how similar these two are.
Once you have your asynchronous sequence, you can loop over its values by using for await
in an async context, like this:
func printAllDoubles() async {
for await number in DoubleGenerator() {
print(number)
}
}
The AsyncSequence
protocol also provides default implementations of a variety of common methods, such as map()
, compactMap()
, allSatisfy()
, and more. For example, we could check whether our generator outputs a specific number like this:
func containsExactNumber() async {
let doubles = DoubleGenerator()
let match = await doubles.contains(16_777_216)
print(match)
}
Again, you need to be in an async context to use this.
SE-0310 upgrades Swift’s read-only properties to support the async
and throws
keywords, either individually or together, making them significantly more flexible.
To demonstrate this, we could create a BundleFile
struct that attempts to load the contents of a file in our app’s resource bundle. Because the file might not be there, might be there but can’t be read for some reason, or might be readable but so big it takes time to read, we could mark the contents
property as async throws
like this:
import Foundation
enum FileError: Error {
case missing, unreadable
}
struct BundleFile {
let filename: String
var contents: String {
get async throws {
guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else {
throw FileError.missing
}
do {
return try String(contentsOf: url)
} catch {
throw FileError.unreadable
}
}
}
}
Because contents
is both async and throwing, we must use try await
when trying to read it:
func printHighScores() async throws {
let file = BundleFile(filename: "highscores")
try await print(file.contents)
}
SE-0304 introduced a whole range of approaches to execute, cancel, and monitor concurrent operations in Swift, and builds upon the work introduced by async/await and async sequences.
For easier demonstration purposes, here are a couple of example functions we can work with – an async function to simulate fetching a certain number of weather readings for a particular location, and a synchronous function to calculate which number lies at a particular position in the Fibonacci sequence:
enum LocationError: Error {
case unknown
}
func getWeatherReadings(for location: String) async throws -> [Double] {
switch location {
case "London":
return (1...100).map { _ in Double.random(in: 6...26) }
case "Rome":
return (1...100).map { _ in Double.random(in: 10...32) }
case "San Francisco":
return (1...100).map { _ in Double.random(in: 12...20) }
default:
throw LocationError.unknown
}
}
func fibonacci(of number: Int) -> Int {
var first = 0
var second = 1
for _ in 0..<number {
let previous = first
first = second
second = previous + first
}
return first
}
The simplest async approach introduced by structured concurrency is the ability to use the @main
attribute to go immediately into an async context, which is done simply by marking the main()
method with async
, like this:
@main
struct Main {
static func main() async throws {
let readings = try await getWeatherReadings(for: "London")
print("Readings are: \(readings)")
}
}
The main changes introduced by structured concurrency are backed by two new types, Task
and TaskGroup
, which allow us to run concurrent operations either individually or in a coordinated way.
In its simplest form, you can start concurrent work by creating a new Task
object and passing it the operation you want to run. This will start running on a background thread immediately, and you can use await
to wait for its finished value to come back.
So, we might call fibonacci(of:)
many times on a background thread, in order to calculate the first 50 numbers in the sequence:
func printFibonacciSequence() async {
let task1 = Task { () -> [Int] in
var numbers = [Int]()
for i in 0..<50 {
let result = fibonacci(of: i)
numbers.append(result)
}
return numbers
}
let result1 = await task1.value
print("The first 50 numbers in the Fibonacci sequence are: \(result1)")
}
As you can see, I’ve needed to explicitly write Task { () -> [Int] in
so that Swift understands that the task is going to return, but if your task code is simpler that isn’t needed. For example, we could have written this and gotten exactly the same result:
let task1 = Task {
(0..<50).map(fibonacci)
}
Again, the task starts running as soon as it’s created, and the printFibonacciSequence()
function will continue running on whichever thread it was while the Fibonacci numbers are being calculated.
Tip: Our task's operation is a non-escaping closure because the task immediately runs it rather than storing it for later, which means if you use Task
inside a class or a struct you don’t need to use self
to access properties or methods.
When it comes to reading the finished numbers, await task1.value
will make sure execution of printFibonacciSequence()
pauses until the task’s output is ready, at which point it will be returned. If you don’t actually care what the task returns – if you just want the code to start running and finish whenever – you don’t need to store the task anywhere.
For task operations that throw uncaught errors, reading your task’s value
property will automatically also throw errors. So, we could write a function that performs two pieces of work at the same time then waits for them both to complete:
func runMultipleCalculations() async throws {
let task1 = Task {
(0..<50).map(fibonacci)
}
let task2 = Task {
try await getWeatherReadings(for: "Rome")
}
let result1 = await task1.value
let result2 = try await task2.value
print("The first 50 numbers in the Fibonacci sequence are: \(result1)")
print("Rome weather readings are: \(result2)")
}
Swift provides us with the built-in task priorities of high
, default
, low
, and background
. The code above doesn’t specifically set one so it will get default
, but we could have said something like Task(priority: .high)
to customize that. If you’re writing just for Apple’s platforms, you can also use the more familiar priorities of userInitiated
in place of high, and utility
in place of low
, but you can’t access userInteractive
because that is reserved for the main thread.
As well as just running operations, Task
also provides us with a handful of static methods to control the way our code runs:
Task.sleep()
will cause the current task to sleep for a specific number of nanoseconds. Until something better comes along, this means writing 1_000_000_000 to mean 1 second.Task.checkCancellation()
will check whether someone has asked for this task to be cancelled by calling its cancel()
method, and if so throw a CancellationError
.Task.yield()
will suspend the current task for a few moments in order to give some time to any tasks that might be waiting, which is particularly important if you’re doing intensive work in a loop.You can see both sleeping and cancellation in the following code example, which puts a task to sleep for one second then cancels it before it completes:
func cancelSleepingTask() async {
let task = Task { () -> String in
print("Starting")
try await Task.sleep(nanoseconds: 1_000_000_000)
try Task.checkCancellation()
return "Done"
}
// The task has started, but we'll cancel it while it sleeps
task.cancel()
do {
let result = try await task.value
print("Result: \(result)")
} catch {
print("Task was cancelled.")
}
}
In that code, Task.checkCancellation()
will realize the task has been cancelled and immediately throw CancellationError
, but that won’t reach us until we attempt to read task.value
.
Tip: Use task.result
to get a Result
value containing the task’s success and failure values. For example, in the code above we’d get back a Result<String, Error>
. This does not require a try
call because you still need to handle the success or failure case.
For more complex work, you should create task groups instead – collections of tasks that work together to produce a finished value.
To minimize the risk of programmers using task groups in dangerous ways, they don’t have a simple public initializer. Instead, task groups are created using functions such as withTaskGroup()
: call this with the body of work you want done, and you’ll be passed in the task group instance to work with. Once inside the group you can add work using the addTask()
method, and it will start executing immediately.
Important: You should not attempt to copy that task group outside the body of withTaskGroup()
– the compiler can’t stop you, but you’re just going to make problems for yourself.
To see a simple example of how task groups work – along with demonstrating an important point of how they order their operations, try this:
func printMessage() async {
let string = await withTaskGroup(of: String.self) { group -> String in
group.addTask { "Hello" }
group.addTask { "From" }
group.addTask { "A" }
group.addTask { "Task" }
group.addTask { "Group" }
var collected = [String]()
for await value in group {
collected.append(value)
}
return collected.joined(separator: " ")
}
print(string)
}
That creates a task group designed to produce one finished string, then queues up several closures using the addTask()
method of the task group. Each of those closures returns a single string, which then gets collected into an array of strings, before being joined into one single string and returned for printing.
Tip: All tasks in a task group must return the same type of data, so for complex work you might find yourself needing to return an enum with associated values in order to get exactly what you want. A simpler alternative is introduced in a separate Async Let Bindings proposal.
Each call to addTask()
can be any kind of function you like, as long as it results in a string. However, although task groups automatically wait for all the child tasks to complete before returning, when that code runs it’s a bit of a toss up what it will print because the child tasks can complete in any order – we’re as likely to get “Hello From Task Group A” as we are “Hello A Task Group From”, for example.
If your task group is executing code that might throw, you can either handle the error directly inside the group or let it bubble up outside the group to be handled there. That latter option is handled using a different function, withThrowingTaskGroup()
, which must be called with try
if you haven’t caught all the errors you throw.
For example, this next code sample calculates weather readings for several locations in a single group, then returns the overall average for all locations:
func printAllWeatherReadings() async {
do {
print("Calculating average weather…")
let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in
group.addTask {
try await getWeatherReadings(for: "London")
}
group.addTask {
try await getWeatherReadings(for: "Rome")
}
group.addTask {
try await getWeatherReadings(for: "San Francisco")
}
// Convert our array of arrays into a single array of doubles
let allValues = try await group.reduce([], +)
// Calculate the mean average of all our doubles
let average = allValues.reduce(0, +) / Double(allValues.count)
return "Overall average temperature is \(average)"
}
print("Done! \(result)")
} catch {
print("Error calculating data.")
}
}
In that instance, each of the calls to addTask()
is identical apart from the location string being passed in, so you can use something like for location in ["London", "Rome", "San Francisco"] {
to call addTask()
in a loop.
Task groups have a cancelAll()
method that cancels any tasks inside the group, but using addTask()
afterwards will continue to add work to the group. As an alternative, you can use addTaskUnlessCancelled()
to skip adding work if the group has been cancelled – check its returned Boolean to see whether the work was added successfully or not.
async let
bindingsSE-0317 introduced the ability to create and await child tasks using the simple syntax async let
. This is particularly useful as an alternative to task groups where you’re dealing with heterogeneous result types – i.e., if you want tasks in a group to return different kinds of data.
To demonstrate this, we could create a struct that has three different types of properties that will come from three different async functions:
struct UserData {
let name: String
let friends: [String]
let highScores: [Int]
}
func getUser() async -> String {
"Taylor Swift"
}
func getHighScores() async -> [Int] {
[42, 23, 16, 15, 8, 4]
}
func getFriends() async -> [String] {
["Eric", "Maeve", "Otis"]
}
If we wanted to create a User
instance from all three of those values, async let
is the easiest way – it run each function concurrently, wait for all three to finish, then use them to create our object.
Here’s how it looks:
func printUserDetails() async {
async let username = getUser()
async let scores = getHighScores()
async let friends = getFriends()
let user = await UserData(name: username, friends: friends, highScores: scores)
print("Hello, my name is \(user.name), and I have \(user.friends.count) friends!")
}
Important: You can only use async let
if you are already in an async context, and if you don’t explicitly await the result of an async let
Swift will implicitly wait for it when exiting its scope.
When working with throwing functions, you don’t need to use try
with async let
– that can automatically be pushed back to where you await the result. Similarly, the await
keyword is also implied, so rather than typing try await someFunction()
with an async let
you can just write someFunction()
.
To demonstrate this, we could write an async function to recursively calculate numbers in the Fibonacci sequence. This approach is hopelessly naive because without memoization we’re just repeating vast amounts of work, so to avoid causing everything to grind to a halt we’re going to limit the input range from 0 to 22:
enum NumberError: Error {
case outOfRange
}
func fibonacci(of number: Int) async throws -> Int {
if number < 0 || number > 22 {
throw NumberError.outOfRange
}
if number < 2 { return number }
async let first = fibonacci(of: number - 2)
async let second = fibonacci(of: number - 1)
return try await first + second
}
In that code the recursive calls to fibonacci(of:)
are implicitly try await fibonacci(of:)
, but we can leave them off and handle them directly on the following line.
SE-0300 introduced new functions to help us adapt older, completion handler-style APIs to modern async code.
For example, this function returns its values asynchronously using a completion handler:
import Foundation
func fetchLatestNews(completion: @escaping ([String]) -> Void) {
DispatchQueue.main.async {
completion(["Swift 5.5 release", "Apple acquires Apollo"])
}
}
If you wanted to use that using async/await you might be able to rewrite the function, but there are various reasons why that might not be possible – it might come from an external library, for example.
Continuations allow us to create a shim between the completion handler and async functions so that we wrap up the older code in a more modern API. For example, the withCheckedContinuation()
function creates a new continuation that can run whatever code you want, then call resume(returning:)
to send a value back whenever you’re ready – even if that’s part of a completion handler closure.
So, we could make a second fetchLatestNews()
function that is async, wrapping around the older completion handler function:
func fetchLatestNews() async -> [String] {
await withCheckedContinuation { continuation in
fetchLatestNews { items in
continuation.resume(returning: items)
}
}
}
With that in place we can now get our original functionality in an async function, like this:
func printNews() async {
let items = await fetchLatestNews()
for item in items {
print(item)
}
}
The term “checked” continuation means that Swift is performing runtime checks on our behalf: are we calling resume()
once and only once? This is important, because if you never resume the continuation then you will leak resources, but if you call it twice then you’re likely to hit problems.
Important: To be crystal clear, you must resume your continuation exactly once.
As there is a runtime performance cost of checking your continuations, Swift also provides a withUnsafeContinuation()
function that works in exactly the same way except does not perform runtime checks on your behalf. This means Swift won’t warn you if you forget to resume the continuation, and if you call it twice then the behavior is undefined.
Because these two functions are called in the same way, you can switch between them easily. So, it seems likely people will use withCheckedContinuation()
while writing their functions so Swift will emit warnings and even trigger crashes if the continuations are used incorrectly, but some may then switch over to withUnsafeContinuation()
as they prepare to ship if they are affected by the runtime performance cost of checked continuations.
SE-0306 introduced actors, which are conceptually similar to classes that are safe to use in concurrent environments. This is possible because Swift ensures that mutable state inside your actor is only ever accessed by a single thread at any given time, which helps eliminate a variety of serious bugs right at the compiler level.
To demonstrate the problem actors solve, consider this Swift code that creates a RiskyCollector
class able to trade cards from their deck with another collector:
class RiskyCollector {
var deck: Set<String>
init(deck: Set<String>) {
self.deck = deck
}
func send(card selected: String, to person: RiskyCollector) -> Bool {
guard deck.contains(selected) else { return false }
deck.remove(selected)
person.transfer(card: selected)
return true
}
func transfer(card: String) {
deck.insert(card)
}
}
In a single-threaded environment that code is safe: we check whether our deck contains the card in question, remove it, then add it to the other collector’s deck. However, in a multi-threaded environment our code has a potential race condition, which is a problem whereby the results of the code will vary as two separate parts of our code run side by side.
If we call send(card:to:)
more than once at the same time, the following chain of events can happen:
In that situation one player loses a card while the other gains two cards, and if that card happened to be a Black Lotus from Magic the Gathering then you’ve got a big problem!
Actors solve this problem by introducing actor isolation: stored properties and methods cannot be read from outside the actor object unless they are performed asynchronously, and stored properties cannot be written from outside the actor object at all. The async behavior isn’t there for performance; instead it’s because Swift automatically places these requests into a queue that is processed sequentially to avoid race conditions.
So, we could rewrite out RiskyCollector
class to be a SafeCollector
actor, like this:
actor SafeCollector {
var deck: Set<String>
init(deck: Set<String>) {
self.deck = deck
}
func send(card selected: String, to person: SafeCollector) async -> Bool {
guard deck.contains(selected) else { return false }
deck.remove(selected)
await person.transfer(card: selected)
return true
}
func transfer(card: String) {
deck.insert(card)
}
}
There are several things to notice in that example:
actor
keyword. This is a new concrete nominal type in Swift, joining structs, classes, and enums.send()
method is marked with async
, because it will need to suspend its work while waiting for the transfer to complete.transfer(card:)
method is not marked with async
, we still need to call it with await
because it will wait until the other SafeCollector
actor is able to handle the request.To be clear, an actor can use its own properties and methods freely, asynchronously or otherwise, but when interacting with a different actor it must always be done asynchronously. With these changes Swift can ensure that all actor-isolated state is never accessed concurrently, and more importantly this is done at compile time so that safety is guaranteed.
Actors and classes have some similarities:
self
and therefore don’t get isolated.Beyond actor isolation, there are two other important differences between actors and classes:
final
keyword, and more. This might change in the future.Actor
protocol; no other concrete type can use this. This allows you to restrict other parts of your code so it can work only with actors.The best way I’ve heard to explain how actors differ from classes is this: “actors pass messages, not memory.” So, rather than one actor poking directly around in another’s properties or calling their methods, we instead send a message asking for the data and let the Swift runtime handle it for us safely.
SE-0316 allows global state to be isolated from data races by using actors.
Although in theory this could result in many global actors, the main benefit at least right now is the introduction of an @MainActor
global actor you can use to mark properties and methods that should be accessed only on the main thread.
As an example, we might have a class to handle data storage in our app, and for safety reasons we refuse to write out change to persistent storage unless we’re on the main thread:
import Foundation
class OldDataController {
func save() -> Bool {
guard Thread.isMainThread else {
return false
}
print("Saving data…")
return true
}
}
That works, but with @MainActor
we can guarantee that save()
is always called on the main thread as if we specifically ran it using DispatchQueue.main
:
class NewDataController {
@MainActor func save() {
print("Saving data…")
}
}
That’s all it takes – Swift will make sure whenever you call save()
on a data controller, that work will happen on the main thread.
Note: Because we’re pushing work through an actor, you must call save()
using await
, async let
, or similar.
@MainActor
is a global actor wrapper around the underlying MainActor
struct, which is helpful because it has a static run()
method that lets us schedule work to be run. This will execute your code on the main thread, optionally sending back a result.
SE-0302 adds support for “sendable” data, which is data that can safely be transferred to another thread. This is accomplished through a new Sendable
protocol, and an @Sendable
attribute for functions.
Many things are inherently safe to send across threads:
Bool
, Int
, String
, and similar.Array<String>
or Dictionary<Int, String>
.String.self
.These have been updated to conform to the Sendable
protocol.
As for custom types, it depends what you’re making:
Sendable
because they handle their synchronization internally.Sendable
if they contain only values that also conform to Sendable
, similar to how Codable
works.Sendable
as long as they either inherits from NSObject
or from nothing at all, all properties are constant and themselves conform to Sendable
, and they are marked as final
to stop further inheritance.Swift lets us use the @Sendable
attribute on functions or closure to mark them as working concurrently, and will enforce various rules to stop us shooting ourself in the foot. For example, the operation we pass into the Task
initializer is marked @Sendable
, which means this kind of code is allowed because the value captured by Task
is a constant:
func printScore() async {
let score = 1
Task { print(score) }
Task { print(score) }
}
However, that code would not be allowed if score
were a variable, because it could be accessed by one of the tasks while the other was changing its value.
You can mark your own functions and closures using @Sendable
, which will enforce similar rules around captured values:
import Foundation
func runLater(_ function: @escaping @Sendable () -> Void) -> Void {
DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function)
}
#if
for postfix member expressionsSE-0308 allows Swift to use #if
conditions with postfix member expressions. This sounds a bit obscure, but it solves a problem commonly seen with SwiftUI: you can now optionally add modifiers to a view.
For example, this change allows us to create a text view with two different font sizes depending on whether we’re using iOS or another platform:
import SwiftUI
Text("Welcome")
#if os(iOS)
.font(.largeTitle)
#else
.font(.headline)
#endif
You can nest these if you want, although it’s a bit hard on your eyes:
Text("Welcome")
#if os(iOS)
.font(.largeTitle)
#if DEBUG
.foregroundColor(.red)
#endif
#else
.font(.headline)
#endif
You could use wildly different postfix expressions if you wanted:
let result = [1, 2, 3]
#if os(iOS)
.count
#else
.reduce(0, +)
#endif
print(result)
Technically you could make result
end up as two completely different types if you wanted, but that seems like a bad idea. What you definitely can’t do is use other kinds of expressions such as using + [4]
instead of .count
– if it doesn’t start with .
then it’s not a postfix member expression.
CGFloat
and Double
typesSE-0307 introduced a small but important quality of life improvement: Swift is able to implicitly convert between CGFloat
and Double
in most places where it is needed.
In its simplest form, this means we can add a CGFloat
and a Double
together to produce a new Double
, like this:
import Foundation
let first: CGFloat = 42
let second: Double = 19
let result = first + second
print(result)
Swift implements this by inserting an implicit initializer as needed, and it will always prefer Double
if it’s possible. More importantly, none of this is achieved by rewriting existing APIs: technically things like scaleEffect()
in SwiftUI still work with CGFloat
, but Swift quietly bridges this to Double
.
SE-0295 upgrades Swift’s Codable
system to support writing enums with associated values. Previously enums were only supported if they conformed to RawRepresentable
, but this extends support to general enums as well as enum cases with any number of Codable
associated values.
For example, we could define a Weather
enum like this one:
enum Weather: Codable {
case sun
case wind(speed: Int)
case rain(amount: Int, chance: Int)
}
That has one simple case, one case with a single associated values, and a third case with two associated values – all are integers, but you could use strings or other Codable
types.
With that enum defined, we can create an array of weather to make a forecast, then use JSONEncoder
or similar and convert the result to a printable string:
import Foundation
let forecast: [Weather] = [
.sun,
.wind(speed: 10),
.sun,
.rain(amount: 5, chance: 50)
]
do {
let result = try JSONEncoder().encode(forecast)
let jsonString = String(decoding: result, as: UTF8.self)
print(jsonString)
} catch {
print("Encoding error: \(error.localizedDescription)")
}
Behind the scenes, this is implemented using multiple CodingKey
enums capable of handling the nested structure that results from having values attached to enum cases, which means writing your own custom coding methods to do the same is a little more work.
lazy
now works in local contextsThe lazy
keyword has always allowed us to write stored properties that are only calculated when first used, but from Swift 5.5 onwards we can use lazy
locally inside a function to create values that work similarly.
This code demonstrates local lazy
in action:
func printGreeting(to: String) -> String {
print("In printGreeting()")
return "Hello, \(to)"
}
func lazyTest() {
print("Before lazy")
lazy var greeting = printGreeting(to: "Paul")
print("After lazy")
print(greeting)
}
lazyTest()
When that runs you’ll see “Before lazy” and “After lazy” printed first, followed by “In printGreeting()” then “Hello, Paul” – Swift only runs the printGreeting(to:)
code when its result is accessed on the print(greeting)
line.
In practice, this feature is going to be really helpful as a way of selectively running code when you have conditions in place: you can prepare the result of some work lazily, and only actual perform the work if it’s still needed later on.
SE-0293 extends property wrappers so they can be applied to parameters for functions and closures. Parameters passed this way are still immutable unless you take a copy of them, and you are still able to access the underlying property wrapper type using a leading underscore if you want.
As an example, we could write a function that accepts an integer and prints it out:
func setScore1(to score: Int) {
print("Setting score to \(score)")
}
When that’s called we can pass it any range of values, like this:
setScore1(to: 50)
setScore1(to: -50)
setScore1(to: 500)
If we wanted our scores to lie only within the range 0...100 we could write a simple property wrapper that clamps numbers as they are created:
@propertyWrapper
struct Clamped<T: Comparable> {
let wrappedValue: T
init(wrappedValue: T, range: ClosedRange<T>) {
self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
Now we can write and call a new function using that wrapper:
func setScore2(@Clamped(range: 0...100) to score: Int) {
print("Setting score to \(score)")
}
setScore2(to: 50)
setScore2(to: -50)
setScore2(to: 500)
Calling setScore2()
with the same input values as before will print different output, because the numbers will get clamped to 50, 0, 100.
Tip: Our property wrapper is trivial because parameters passed into a function are immutable – we don’t need to handle re-clamping the wrapped value when it changes because it won’t change. However, you can make your property wrappers as complex as you need; they work just as they would with properties or local variables.
SE-0299 allows Swift to perform static member lookup for members of protocols in generic functions, which sounds obscure but actually fixes a small but important legibility problem that hit SwiftUI particularly hard.
At this time SwiftUI hasn’t been updated to support this change, but if everything goes to plan we can stop writing this:
import SwiftUI
Toggle("Example", isOn: .constant(true))
.toggleStyle(SwitchToggleStyle())
And instead write something like this:
Toggle("Example", isOn: .constant(true))
.toggleStyle(.switch)
This was possible in early SwiftUI betas because Apple had put extensive workarounds in place, but these were withdrawn before release.
To see what’s actually changing here, imagine a Theme
protocol with several structs conforming to it:
protocol Theme { }
struct LightTheme: Theme { }
struct DarkTheme: Theme { }
struct RainbowTheme: Theme { }
We could also define a Screen
protocol that is able to have a theme()
method called on it with some sort of theme:
protocol Screen { }
extension Screen {
func theme<T: Theme>(_ style: T) -> Screen {
print("Activating new theme!")
return self
}
}
And now we could create an instance of a screen:
struct HomeScreen: Screen { }
Following older SwiftUI code, we could enable a light theme on that screen by specifying LightTheme()
:
let lightScreen = HomeScreen().theme(LightTheme())
If we wanted to make that easier to access, we could try adding a static light
property to our Theme
protocol like this:
extension Theme where Self == LightTheme {
static var light: LightTheme { .init() }
}
However, using that with the theme()
method of our generic protocol was what caused the problem: before Swift 5.5 it was not possible and you had to use LightTheme()
every time. However, in Swift 5.5 or later this is now possible:
let lightTheme = HomeScreen().theme(.light)
any
SE-0335 introduces a new any
keyword to mark existential types, and although that might sound esoteric please don’t skip ahead: this one is a big change, and is likely to break a lot of Swift code in future versions.
Protocols allow us to specify a set of requirements that conforming types must adhere to, such as methods they must implement. So, we often write code like this:
protocol Vehicle {
func travel(to destination: String)
}
struct Car: Vehicle {
func travel(to destination: String) {
print("I'm driving to \(destination)")
}
}
let vehicle = Car()
vehicle.travel(to: "London")
It’s also possible to use protocols as generic type constraints in functions, meaning that we write code that can work with any kind of data that conforms to a particular protocol. For example, this will work with any kind of type that conforms to Vehicle
:
func travel<T: Vehicle>(to destinations: [String], using vehicle: T) {
for destination in destinations {
vehicle.travel(to: destination)
}
}
travel(to: ["London", "Amarillo"], using: vehicle)
When that code compiles, Swift can see we’re calling travel()
with a Car
instance and so it is able to create optimized code to call the travel()
function directly – a process known as static dispatch.
All this matters because there is a second way to use protocols, and it looks very similar to the other code we’ve used so far:
let vehicle2: Vehicle = Car()
vehicle2.travel(to: "Glasgow")
Here we are still creating a Car
struct, but we’re storing it as a Vehicle
. This isn’t just a simple matter of hiding the underlying information, but instead this Vehicle
type is a whole other thing called an existential type: a new data type that is able to hold any value of any type that conforms to the Vehicle
protocol.
Important: Existential types are different from opaque types that use the some
keyword, e.g. some View
, which must always represent one specific type that conforms to whatever constraints you specify.
We can use existential types with functions too, like this:
func travel2(to destinations: [String], using vehicle: Vehicle) {
for destination in destinations {
vehicle.travel(to: destination)
}
}
That might look similar to the other travel()
function, but as this one accepts any kind of Vehicle
object Swift can no longer perform the same set of optimizations – it has to use a process called dynamic dispatch, which is less efficient than the static dispatch available in the generic equivalent. So, Swift was in a position where both uses of protocols looked very similar, and actually the slower, existential version of our function was easier to write.
To resolve this problem, Swift 5.6 introduces a new any
keyword for use with existential types, so that we’re explicitly acknowledging the impact of existentials in our code. In Swift 5.6 this new behavior is enabled and works, but in future Swift versions failing to use it will generate warnings, and from Swift 6 onwards the plan is to issue errors – you will be required to mark existential types using any
.
So, you would write this:
let vehicle3: any Vehicle = Car()
vehicle3.travel(to: "Glasgow")
func travel3(to destinations: [String], using vehicle: any Vehicle) {
for destination in destinations {
vehicle.travel(to: destination)
}
}
I know it took a lot of explanation to reach this conclusion, but hopefully it makes sense: when we use Vehicle
as a conformance or a generic constraint we will carry on writing Vehicle
, but when we use it as its own type we should start moving across to any Vehicle
.
This is a big breaking change in Swift. Fortunately, like I said the Swift team are taking it slow – here’s what they said in the acceptance decision:
“The goal is that that one can write code that compiles without warnings for the current Swift release and at least one major release prior, after which warnings can be introduced to guide users to the new syntax in existing language modes. Finally, the old syntax can be removed or repurposed only in a new major language version.”
SE-0315 introduces the concept of type placeholders, which allow us to explicitly specify only some parts of a value’s type so that the remainder can be filled in using type inference.
In practice, this means writing _
as your type in any place you want Swift to use type inference, meaning that these three lines of code are the same:
let score1 = 5
let score2: Int = 5
let score3: _ = 5
In those trivial examples type placeholders don’t add anything, but they are useful when the compiler is able to correctly infer part of a type but not all. For example, if you were creating a dictionary of student names and all the exam results they had this year, you might write this:
var results1 = [
"Cynthia": [],
"Jenny": [],
"Trixie": [],
]
Swift will infer that to be a dictionary with strings as keys, and an array of Any
as values – almost certainly not what you want. You could specify the entire type explicitly, like this:
var results2: [String: [Int]] = [
"Cynthia": [],
"Jenny": [],
"Trixie": [],
]
However, type placeholders allow you to write _
in place of the parts you want the compiler to infer – it’s a way for us to explicitly say “this part should use type inference”, alongside places where we want an exact type of our choosing.
So, we could also write this:
var results3: [_: [Int]] = [
"Cynthia": [],
"Jenny": [],
"Trixie": [],
]
As you can see, the _
there is an explicit request for type inference, but we still have the opportunity to specify the exact array type.
Tip: Type placeholders can be optional too – use _?
to have Swift infer your type as optional.
Types placeholders do not affect the way we write function signatures: you must still provide their parameter and return types in full. However, I have found that type placeholders do still serve a purpose for when you’re busy experimenting with a prototype: telling the compiler you want it to infer some type often prompts Xcode to offer a Fix-it to complete the code for you.
For example, you might write code to create a player like this:
struct Player<T: Numeric> {
var name: String
var score: T
}
func createPlayer() -> _ {
Player(name: "Anonymous", score: 0)
}
That fails to specify a return type for createPlayer()
, which will cause a compiler error. However, as we’ve asked Swift to infer the type, the error in Xcode will offer a Fix-it to replace _
with Player<Int>
– you can imagine that saving a fair amount of hassle when dealing with more complex types.
Think of type placeholders as a way of simplifying long type annotations: you can replace all the less relevant or boilerplate parts with underscores, leaving the important parts spelled out to help make your code more readable.
String
/Int
keyed Dictionary
into a KeyedContainer
SE-0320 introduces a new CodingKeyRepresentable
protocol that allows dictionaries with keys that aren’t a plain String
or Int
to be encoded as keyed containers rather than unkeyed containers.
To understand why this is important, you first need to see the behavior without CodingKeyRepresentable
in place. As an example, this old code uses enum cases for keys in a dictionary, then encodes it to JSON and prints out the resulting string:
import Foundation
enum OldSettings: String, Codable {
case name
case twitter
}
let oldDict: [OldSettings: String] = [.name: "Paul", .twitter: "@twostraws"]
let oldData = try JSONEncoder().encode(oldDict)
print(String(decoding: oldData, as: UTF8.self))
Although the enum has a String
raw value, because the dictionary keys aren’t String
or Int
the resulting string will be ["twitter","@twostraws","name","Paul"] – four separate string values, rather than something that is obviously key/value pairs. Swift is smart enough to recognize this in decoding, and will match alternating strings inside each pair to the original enum keys and string values, but this isn’t helpful if you want to send the JSON to a server.
The new CodingKeyRepresentable
resolves this, allowing the new dictionary keys to be written correctly. However, as this changes the way your Codable
JSON is written, you must explicitly add CodingKeyRepresentable
conformance to get the new behavior, like this:
enum NewSettings: String, Codable, CodingKeyRepresentable {
case name
case twitter
}
let newDict: [NewSettings: String] = [.name: "Paul", .twitter: "@twostraws"]
let newData = try! JSONEncoder().encode(newDict)
print(String(decoding: newData, as: UTF8.self))
That will print {"twitter":"@twostraws","name":"Paul”}, which is much more useful outside of Swift.
If you’re using custom structs as your keys, you can also conform to CodingKeyRepresentable
and provide your own methods for converting your data into a string.
SE-0290 introduces an inverted form of #available
called #unavailable
, which will run some code if an availability check fails.
This is going to be particularly useful in places where you were previously using #available
with an empty true block because you only wanted to run code if a specific operating system was unavailable. So, rather than writing code like this:
if #available(iOS 15, *) { } else {
// Code to make iOS 14 and earlier work correctly
}
We can now write this:
if #unavailable(iOS 15) {
// Code to make iOS 14 and earlier work correctly
}
This problem wasn’t just theoretical – using an empty #available
block was a fairly common workaround, particularly in the transition to the scene-based UIKit APIs in iOS 13.
Apart from their flipped behavior, one key difference between #available
and #unavailable
is the platform wildcard, *
. This is required with #available
to allow for potential future platforms, which meant that writing if #available(iOS 15, *)
would mark some code as being available on iOS versions 15 or later, or all other platforms – macOS, tvOS, watchOS, and any future unknown platforms.
With #unavailable
, this behavior no longer makes sense: would writing if #unavailable(iOS 15, *)
mean “the following code should be run on iOS 14 and earlier,” or should it also include macOS, tvOS, watchOS, Linux, and more, where iOS 15 is also unavailable? To avoid this ambiguity, the platform wildcard is not allowed with #unavailable
: only platforms you specifically list are considered for the test.
Swift 5.5 added a lot of features around concurrency, and 5.6 continues the process of refining those features to make them safer and more consistent, while also working towards bigger, breaking changes coming in Swift 6.
The biggest change is SE-0337, which aims to provide a roadmap towards full, strict concurrency checking for our code. This is designed to be incremental: you can import whole modules using @preconcurrency
to tell Swift the module was created without modern concurrency in mind; or you can mark individual classes, structs, properties, methods and more as @preconcurrency
to be more selective.
In the short term this makes it significantly easier to migrate larger projects to modern concurrency.
Another area that’s changing is the use of actors, because as a result of SE-0327 Swift 5.6 now issues a warning if you attempt to instantiate a @MainActor
property using @StateObject
like this:
import SwiftUI
@MainActor class Settings: ObservableObject { }
struct OldContentView: View {
@StateObject private var settings = Settings()
var body: some View {
Text("Hello, world!")
}
}
This warning will be upgraded to an error in Swift 6, so you should be prepared to move away from this code and use this instead:
struct NewContentView: View {
@StateObject private var settings: Settings
init() {
_settings = StateObject(wrappedValue: Settings())
}
var body: some View {
Text("Hello, world!")
}
}
Swift 5.6 includes a raft ([1], [2], [3], [4]) of improvements to Swift Package Manager, which combine to add the beginnings of plugin support using external build tools.
At this point earlier adopters are reporting that the functionality isn’t quite far along enough to support more complex use cases, but it does at least seem to support SwiftGen and there are further examples demonstrating possibilities for DocC and swift-format.
SE-0345 introduces new shorthand syntax for unwrapping optionals into shadowed variables of the same name using if let
and guard let
. This means we can now write code like this:
var name: String? = "Linda"
if let name {
print("Hello, \(name)!")
}
Whereas previously we would have written code more like this:
if let name = name {
print("Hello, \(name)!")
}
if let unwrappedName = name {
print("Hello, \(unwrappedName)!")
}
This change doesn’t extend to properties inside objects, which means code like this will not work:
struct User {
var name: String
}
let user: User? = User(name: "Linda")
if let user.name {
print("Welcome, \(user.name)!")
}
SE-0326 dramatically improves Swift’s ability to use parameter and type inference for closures, meaning that many places where we had to specify explicit input and output types can now be removed.
Previously Swift really struggled for any closures that weren’t trivial, but from Swift 5.7 onwards we can now write code like this:
let scores = [100, 80, 85]
let results = scores.map { score in
if score >= 85 {
return "\(score)%: Pass"
} else {
return "\(score)%: Fail"
}
}
Prior to Swift 5.7, we needed to specify the return type explicitly, like this:
let oldResults = scores.map { score -> String in
if score >= 85 {
return "\(score)%: Pass"
} else {
return "\(score)%: Fail"
}
}
SE-0329 introduces a new, standardized way of referring to times and durations in Swift. As the name suggests, it’s broken down into three main components:
The most immediate application of this for many people will be the newly upgraded Task
API, which can now specify sleep amounts in much more sensible terms than nanoseconds:
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
This newer API also comes with the benefit of being able to specify tolerance, which allows the system to wait a little beyond the sleep deadline in order to maximize power efficiency. So, if we wanted to sleep for at least 1 seconds but would be happy for it to last up to 1.5 seconds in total, we would write this:
try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5), clock: .continuous)
Tip: This tolerance is only in addition to the default sleep amount – the system won’t end the sleep before at least 1 second has passed.
Although it hasn’t happened yet, it looks like the older nanoseconds-based API will be deprecated in the near future.
Clocks are also useful for measuring some specific work, which is helpful if you want to show your users something like how long a file export took:
let clock = ContinuousClock()
let time = clock.measure {
// complex work here
}
print("Took \(time.components.seconds) seconds")
Swift 5.7 introduces a whole raft of improvements relating to regular expressions (regexes), and in doing so dramatically improves the way we process strings. This is actually a whole chain of interlinked proposals, including
Regex
type/.../
rather than going through Regex
and a string.Put together this is pretty revolutionary for strings in Swift, which have often been quite a sore point when compared to other languages and platforms.
To see what’s changing, let’s start simple and work our way up.
First, we can now draw on a whole bunch of new string methods, like so:
let message = "the cat sat on the mat"
print(message.ranges(of: "at"))
print(message.replacing("cat", with: "dog"))
print(message.trimmingPrefix("the "))
But the real power of these is that they all accept regular expressions too:
print(message.ranges(of: /[a-z]at/))
print(message.replacing(/[a-m]at/, with: "dog"))
print(message.trimmingPrefix(/The/.ignoresCase()))
In case you’re not familiar with regular expressions:
Notice how each of those regexes are made using regex literals – the ability to create a regular expression by starting and ending your regex with a /
.
Along with regex literals, Swift provides a dedicated Regex
type that works similarly:
do {
let atSearch = try Regex("[a-z]at")
print(message.ranges(of: atSearch))
} catch {
print("Failed to create regex")
}
However, there’s a key difference that has significant side effects for our code: when we create a regular expression from a string using Regex
, Swift must parse the string at runtime to figure out the actual expression it should use. In comparison, using regex literals allows Swift to check your regex at compile time: it can validate the regex contains no errors, and also understand exactly what matches it will contain.
This bears repeating, because it’s quite remarkable: Swift parses your regular expressions at compile time, making sure they are valid – this is, for me at least, the coding equivalent of the head explode emoji.
To see how powerful this difference is, consider this code:
let search1 = /My name is (.+?) and I'm (\d+) years old./
let greeting1 = "My name is Taylor and I'm 26 years old."
if let result = try? search1.wholeMatch(in: greeting1) {
print("Name: \(result.1)")
print("Age: \(result.2)")
}
That creates a regex looking for two particular values in some text, and if it finds them both prints them. But notice how the result
tuple can reference its matches as .1
and .2
, because Swift knows exactly which matches will occur. (In case you were wondering, .0
will return the whole matched string.)
In fact, we can go even further because regular expressions allow us to name our matches, and these flow through to the resulting tuple of matches:
let search2 = /My name is (?<name>.+?) and I'm (?<age>\d+) years old./
let greeting2 = "My name is Taylor and I'm 26 years old."
if let result = try? search2.wholeMatch(in: greeting2) {
print("Name: \(result.name)")
print("Age: \(result.age)")
}
This kind of safety just wouldn’t be possible with regexes created from strings.
But Swift goes one step further: you can create regular expressions from strings, you can create them from regex literals, but you can also create them from a domain-specific language similar to SwiftUI code.
For example, if we wanted to match the same “My name is Taylor and I’m 26 years old” text, we could write a regex like this:
import RegexBuilder
let search3 = Regex {
"My name is "
Capture {
OneOrMore(.word)
}
" and I'm "
Capture {
OneOrMore(.digit)
}
" years old."
}
Even better, this DSL approach is able to apply transformations to the matches it finds, and if we use TryCapture
rather than Capture
then Swift will automatically consider the whole regex not to match if the capture fails or throws an error. So, in the case of our age matching we could write this to convert the age string into an integer:
let search4 = Regex {
"My name is "
Capture {
OneOrMore(.word)
}
" and I'm "
TryCapture {
OneOrMore(.digit)
} transform: { match in
Int(match)
}
" years old."
}
And you can even bring together named matches using variables with specific types like this:
let nameRef = Reference(Substring.self)
let ageRef = Reference(Int.self)
let search5 = Regex {
"My name is "
Capture(as: nameRef) {
OneOrMore(.word)
}
" and I'm "
TryCapture(as: ageRef) {
OneOrMore(.digit)
} transform: { match in
Int(match)
}
" years old."
}
if let result = greeting1.firstMatch(of: search5) {
print("Name: \(result[nameRef])")
print("Age: \(result[ageRef])")
}
Of the three options, I suspect the regex literals will get the most use because it’s the most natural, but helpfully Xcode has the ability to convert regex literals into the RegexBuilder syntax.
SE-0347 expands Swift ability to use default values with generic parameter types. What it allows seems quite niche, but it does matter: if you have a generic type or function you can now provide a concrete type for a default expression, in places where previously Swift would have thrown up a compiler error.
As an example, we might have a function that returns count
number of random items from any kind of sequence:
func drawLotto1<T: Sequence>(from options: T, count: Int = 7) -> [T.Element] {
Array(options.shuffled().prefix(count))
}
That allows us to run a lottery using any kind of sequence, such as an array of names or an integer range:
print(drawLotto1(from: 1...49))
print(drawLotto1(from: ["Jenny", "Trixie", "Cynthia"], count: 2))
SE-0347 extends this to allow us to provide a concrete type as default value for the T
parameter in our function, allowing us to keep the flexibility to use string arrays or any other kind of collection, while also defaulting to the range option that we want most of the time:
func drawLotto2<T: Sequence>(from options: T = 1...49, count: Int = 7) -> [T.Element] {
Array(options.shuffled().prefix(count))
}
And now we can call our function either with a custom sequence, or let the default take over:
print(drawLotto2(from: ["Jenny", "Trixie", "Cynthia"], count: 2))
print(drawLotto2())
SE-0343 upgrades Swift’s support for top-level code – think main.swift in a macOS Command Line Tool project – so that it supports concurrency out of the box. This is one of those changes that might seem trivial on the surface, but took a lot of work to make happen.
In practice, it means you can write code like this directly into your main.swift files:
import Foundation
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
print("Found \(readings.count) temperature readings")
Previously, we had to create a new @main
struct that had an asynchronous main()
method, so this new, simpler approach is a big improvement.
SE-0341 unlocks the ability to use some
with parameter declarations in places where simpler generics were being used.
As an example, if we wanted to write a function that checks whether an array is sorted, Swift 5.7 and later allow us to write this:
func isSorted(array: [some Comparable]) -> Bool {
array == array.sorted()
}
The [some Comparable]
parameter type means this function works with an array containing elements of one type that conforms to the Comparable
protocol, which is syntactic sugar for the equivalent generic code:
func isSortedOld<T: Comparable>(array: [T]) -> Bool {
array == array.sorted()
}
Of course, we could also write the even longer constrained extension:
extension Array where Element: Comparable {
func isSorted() -> Bool {
self == self.sorted()
}
}
This simplified generic syntax does mean we no longer have the ability to add more complex constraints our types, because there is no specific name for the synthesized generic parameter.
Important: You can switch between explicit generic parameters and this new simpler syntax without breaking your API.
SE-0328 widens the range of places that opaque result types can be used.
For example, we can now return more than one opaque type at a time:
import SwiftUI
func showUserDetails() -> (some Equatable, some Equatable) {
(Text("Username"), Text("@twostraws"))
}
We can also return opaque types:
func createUser() -> [some View] {
let usernames = ["@frankefoster", "@mikaela__caron", "@museumshuffle"]
return usernames.map(Text.init)
}
Or even send back a function that itself returns an opaque type when called:
func createDiceRoll() -> () -> some View {
return {
let diceRoll = Int.random(in: 1...6)
return Text(String(diceRoll))
}
}
So, this is another great example of Swift harmonizing the language to make things consistently possible.
SE-0309 significantly loosens Swift’s ban on using protocols as types when they have Self
or associated type requirements, moving to a model where only specific properties or methods are off limits based on what they do.
In simple terms, this means the following code becomes legal:
let firstName: any Equatable = "Paul"
let lastName: any Equatable = "Hudson"
Equatable
is a protocol with Self
requirements, which means it provides functionality that refers to the specific type that adopts it. For example, Int
conforms to Equatable
, so when we say 4 == 4
we’re actually running a function that accepts two integers and returns true if they match.
Swift could implement this functionality using a function similar to func ==(first: Int, second: Int) -> Bool
, but that wouldn’t scale well – they would need to write dozens of such functions to handle Booleans, strings, arrays, and so on. So, instead the Equatable
protocol has a requirement like this: func ==(lhs: Self, rhs: Self) -> Bool
. In English, that means “you need to be able to accept two instances of the same type and tell me if they are the same.” That might be two integers, two strings, two Booleans, or two of any other type that conforms to Equatable
.
To avoid this problem and similar ones, any time Self
appeared in a protocol before Swift 5.7 the compiler would simply not allow us to use it in code such as this:
let tvShow: [any Equatable] = ["Brooklyn", 99]
From Swift 5.7 onwards, this code is allowed, and now the restrictions are pushed back to situations where you attempt to use the type in a place where Swift must actually enforce its restrictions. This means we can’t write firstName == lastName
because as I said ==
must be sure it has two instances of the same type in order to work, and by using any Equatable
we’re hiding the exact types of our data.
However, what we have gained is the ability to do runtime checks on our data to identify specifically what we’re working with. In the case of our mixed array, we could write this:
for item in tvShow {
if let item = item as? String {
print("Found string: \(item)")
} else if let item = item as? Int {
print("Found integer: \(item)")
}
}
Or in the case of our two strings, we could use this:
if let firstName = firstName as? String, let lastName = lastName as? String {
print(firstName == lastName)
}
The key to understanding what this change does is remembering that it allow us to use these protocol more freely, as long as we don’t do anything that specifically needs to knows about the internals of the type. So, we could write code to check whether all items in any sequence conform to the Identifiable
protocol:
func canBeIdentified(_ input: any Sequence) -> Bool {
input.allSatisfy { $0 is any Identifiable }
}
SE-0346 adds newer, simpler syntax for referring to protocols that have specific associated types.
As an example, if we were writing code to cache different kinds of data in different kinds of ways, we might start like this:
protocol Cache<Content> {
associatedtype Content
var items: [Content] { get set }
init(items: [Content])
mutating func add(item: Content)
}
Notice that the protocol now looks like both a protocol and a generic type – it has an associated type declaring some kind of hole that conforming types must fill, but also lists that type in angle brackets: Cache<Content>
.
The part in angle brackets is what Swift calls its primary associated type, and it’s important to understand that not all associated types should be declared up there. Instead, you should list only the ones that calling code normally cares about specifically, e.g. the types of dictionary keys and values or the identifier type in the Identifiable
protocol. In our case we’ve said that our cache’s content – strings, images, users, etc – is its primary associated type.
At this point, we can go ahead and use our protocol as before – we might create some kind of data we want to cache, and then create a concrete cache type conforming to the protocol, like this:
struct File {
let name: String
}
struct LocalFileCache: Cache {
var items = [File]()
mutating func add(item: File) {
items.append(item)
}
}
Now for the clever part: when it comes to creating a cache, we can obviously create a specific one directly, like this:
func loadDefaultCache() -> LocalFileCache {
LocalFileCache(items: [])
}
But very often we want to hide the specifics of what we’re doing, like this:
func loadDefaultCacheOld() -> some Cache {
LocalFileCache(items: [])
}
Using some Cache
gives us the flexibility of changing our mind about what specific cache is sent back, but what SE-0346 lets us do is provide a middle ground between being absolutely specific with a concrete type, and being rather vague with an opaque return type. So, we can specialize the protocol, like this:
func loadDefaultCacheNew() -> some Cache<File> {
LocalFileCache(items: [])
}
So, we’re still retaining the ability to move to a different Cache
-conforming type in the future, but we’ve made it clear that whatever is chosen here will store files internally.
This smarter syntax extends to other places too, including things like extensions:
extension Cache<File> {
func clean() {
print("Deleting all cached files…")
}
}
And generic constraints:
func merge<C: Cache<File>>(_ lhs: C, _ rhs: C) -> C {
print("Copying all files into a new location…")
// now send back a new cache with items from both other caches
return C(items: lhs.items + rhs.items)
}
But what will prove most helpful of all is that SE-0358 brings these primary associated types to Swift’s standard library too, so Sequence
, Collection
, and more will benefit – we can write Sequence<String>
to write code that is agnostic of whatever exact sequence type is being used.
SE-0353 provides the ability to compose SE-0309 (“Unlock existentials for all protocols”) and SE-0346 (“Lightweight same-type requirements for primary associated types”) to write code such as any Sequence<String>
.
It’s a huge feature in its own right, but once you understand the component parts hopefully you can see how it all fits together!
SE-0336 and SE-0344 introduce the ability for actors to work in a distributed form – to read and write properties or call methods over a network using remote procedure calls (RPC).
This is every part as complicated a problem as you might imagine, but there are three things to make it easier:
await
calls we would no matter what, and if the actor happens to be local then the call is handled as a regular local actor function.distributed actor
then distributed func
as needed.So, we can write code like this to simulate someone tracking a trading card system:
// use Apple's ClusterSystem transport
typealias DefaultDistributedActorSystem = ClusterSystem
distributed actor CardCollector {
var deck: Set<String>
init(deck: Set<String>) {
self.deck = deck
}
distributed func send(card selected: String, to person: CardCollector) async -> Bool {
guard deck.contains(selected) else { return false }
do {
try await person.transfer(card: selected)
deck.remove(selected)
return true
} catch {
return false
}
}
distributed func transfer(card: String) {
deck.insert(card)
}
}
Because of the throwing nature of distributed actor calls, we can be sure it’s safe to remove the card from one collector if the call to person.transfer(card:)
didn’t throw.
Swift’s goal is that you can transfer your knowledge of actors over to distributed actors very easily, but there are some important differences that might catch you out.
First, all distributed functions must be called using try
as well as await
even if the function isn’t marked as throwing, because it’s possible for a failure to happen as a result of the network call going awry.
Second, all parameters and return values for distributed methods must conform to a serialization process of your choosing, such as Codable
. This gets checked at compile time, so Swift can guarantee it’s able to send and receive data from remote actors.
And third, you should consider adjusting your actor API to minimize data requests. For example, if you want to read the username
, firstName
, and lastName
properties of a distributed actor, you should prefer to request all three with a single method call rather than requesting them as individual properties to avoid potentially having to go back and forward over the network several times.
SE-0348 dramatically simplifies the overloads required to implement complex result builders, which is part of the reason Swift’s advanced regular expression support was possible. However, it also theoretically removes the 10-view limit for SwiftUI without needing to add variadic generics, so if it’s adopted by the SwiftUI team it will make a lot of folks happy.
To give you a practical example, here’s a simplified version of what SwiftUI’s ViewBuilder
looks like:
import SwiftUI
@resultBuilder
struct SimpleViewBuilderOld {
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View {
TupleView((c0, c1))
}
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
TupleView((c0, c1, c2))
}
}
I’ve made that to include two versions of buildBlock()
: one that accepts two views and one that accepts three. In practice, SwiftUI accepts a wide variety of alternatives, but critically only up to 10 – there’s a buildBlock()
variant that returns TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>
, but there isn’t anything beyond that for practical reasons.
We could then use that result builder with functions or computed properties, like this:
@SimpleViewBuilderOld func createTextOld() -> some View {
Text("1")
Text("2")
Text("3")
}
That will accept all three Text
views using the buildBlock<C0, C1, C2>()
variant, and return a single TupleView
containing them all. However, in this simplified example there’s no way to add a fourth Text
view, because I didn’t provide any more overloads in just the same way that SwiftUI doesn’t support 11 or more.
This is where the new buildPartialBlock()
comes in, because it works like the reduce()
method of sequences: it has an initial value, then updates that by adding whatever it has already to whatever comes next.
So, we could create a new result builder that knows how to accept a single view, and how to combine that view with another one:
@resultBuilder
struct SimpleViewBuilderNew {
static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View {
content
}
static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
TupleView((accumulated, next))
}
}
Even though we only have variants accepting one or two views, because they accumulate we can actually use as many as we want:
@SimpleViewBuilderNew func createTextNew() -> some View {
Text("1")
Text("2")
Text("3")
}
The result isn’t identical, however: in the first example we would get back a TupleView<Text, Text, Text>
, whereas now we would get back a TupleView<(TupleView<(Text, Text)>, Text)>
– one TupleView
nested inside another. Fortunately, if the SwiftUI team do intend to adopt this they ought to be able to create the same 10 buildPartialBlock()
overloads they had before, which should mean the compile automatically creates groups of 10 just like we’re doing explicitly right now.
Tip: buildPartialBlock()
is part of Swift as opposed to any platform-specific runtime, so if you adopt it you’ll find it back deploys to earlier OS releases.
SE-0352 allows Swift to call generic functions using a protocol in many situations, which removes a somewhat odd barrier that existed previously.
As an example, here’s a simple generic function that is able to work with any kind of Numeric
value:
func double<T: Numeric>(_ number: T) -> T {
number * 2
}
If we call that directly, e.g. double(5)
, then the Swift compiler can choose to specialize the function – to effectively create a version that accepts an Int
directly, for performance reasons.
However, what SE-0352 does is allow that function to be callable when all we know is that our data conforms to a protocol, like this:
let first = 1
let second = 2.0
let third: Float = 3
let numbers: [any Numeric] = [first, second, third]
for number in numbers {
print(double(number))
}
Swift calls these existential types: the actual data type you’re using sits inside a box, and when we call methods on that box Swift understands it should implicitly call the method on the data inside the box. SE-0352 extends this same power to function calls too: the number
value in our loop is an existential type (a box containing either an Int
, Double
, or Float
), but Swift is able to pass it in to the generic double()
function by sending in the value inside the box.
There are limits to what this capable of, and I think they are fairly self explanatory. For example, this kind of code won’t work:
func areEqual<T: Numeric>(_ a: T, _ b: T) -> Bool {
a == b
}
print(areEqual(numbers[0], numbers[1]))
Swift isn’t able to statically verify (i.e., at compile time) that both values are things that can be compared using ==
, so the code simply won’t build.
SE-0340 partially closes a potentially risky situation in Swift’s concurrency model, by allowing us to mark types and functions as being unavailable in asynchronous contexts because using them in such a way could cause problems. Unless you’re using thread-local storage, locks, mutexes, or semaphores, it’s unlikely you’ll use this attribute yourself, but you might call code that uses it so it’s worth at least being aware it exists.
To mark something as being unavailable in async context, use @available
with your normal selection of platforms, then add noasync
to the end. For example, we might have a function that works on any platform, but might cause problems when called asynchronously, so we’d mark it like this:
@available(*, noasync)
func doRiskyWork() {
}
We can then call that from a regular synchronous function as normal:
func synchronousCaller() {
doRiskyWork()
}
However, Swift will issue an error if we attempted the same from an asynchronous function, so this code will not work:
func asynchronousCaller() async {
doRiskyWork()
}
This protection is an improvement over the current situation, but should not be leaned on too heavily because it doesn’t stop us from nesting the call to our noasync
function, like this:
func sneakyCaller() async {
synchronousCaller()
}
That runs in an async context, but calls a synchronous function, which can in turn call the noasync
function doRiskyWork()
.
So, noasync
is an improvement, but you still need to be careful when using it. Fortunately, as the Swift Evolution proposal says, “the attribute is expected to be used for a fairly limited set of specialized use-cases” – there’s a good chance you might never come across code that uses it.
SE-0373 relaxes some of the restrictions on variables when used inside result builders, allowing us to write code that would previously have been disallowed by the compiler.
For example, in Swift 5.8 we can use lazy variables directly inside result builders, like so:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
lazy var user = fetchUsername()
Text("Hello, \(user).")
}
.padding()
}
func fetchUsername() -> String {
"@twostraws"
}
}
That shows the concept, but doesn’t provide any benefit because the lazy variable is always used – there’s no difference between using lazy var
and let
in that code. To see where it’s actually useful takes a longer code example, like this one:
// The user is an active subscriber, not an active subscriber, or we don't know their status yet.
enum UserState {
case subscriber, nonsubscriber, unknown
}
// Two small pieces of information about the user
struct User {
var id: UUID
var username: String
}
struct SubscriberView: View {
@State private var state = UserState.unknown
var body: some View {
VStack {
lazy var user = fetchUsername()
switch state {
case .subscriber:
Text("Hello, \(user.username). Here's what's new for subscribers…")
case .nonsubscriber:
Text("Hello, \(user.username). Here's why you should subscribe…")
Button("Subscribe now") {
startSubscription(for: user)
}
case .unknown:
Text("Sign up today!")
}
}
.padding()
}
// Example function that would do complex work
func fetchUsername() -> User {
User(id: UUID(), username: "Anonymous")
}
func startSubscription(for user: User) {
print("Starting subscription…")
}
}
This approach solves problems that would appear in the alternatives:
lazy
, then fetchUsername()
would be called in all three cases of state
, even when it isn’t used in one.lazy
and placed the call to fetchUsername()
inside the two cases then we would be duplicating code – not a massive problem with a simple one liner, but you can imagine how this would cause problems in more complex code.user
out to a computed property, it would be called a second time when the user clicked the "Subscribe now" button.This change also allows us to use property wrappers and local computed properties inside result builders, although I suspect they will be less useful. For example, this kind of code is now allowed:
struct CounterView: View {
var body: some View {
@AppStorage("counter") var tapCount = 0
Button("Count: \(tapCount)") {
tapCount += 1
}
}
}
However, although that will cause the underlying UserDefaults
value to change with each tap, using @AppStorage
in this way won’t cause the body
property to be reinvoked every time tapCount
changes – our UI won’t automatically be updated to reflect the change.
SE-0376 adds a new @backDeployed
attribute that makes it possible to allow new APIs to be used on older versions of frameworks. It works by writing the code for a function into your app’s binary then performing a runtime check: if your user is on a suitably new version of the operating system then the system’s own version of the function will be used, otherwise the version copied into your app’s binary will be used instead.
On the surface this sounds like a fantastic way for Apple to make some new features retroactively available in earlier operating systems, but I don’t think it’s some kind of silver bullet – @backDeployed
applies only to functions, methods, subscripts, and computed properties, so while it might work great for smaller API changes such as the fontDesign()
modifier introduced in iOS 16.1, it wouldn’t work for any code that requires new types to be used, such as the new scrollBounceBehavior()
modifier that relies on a new ScrollBounceBehavior
struct.
As an example, iOS 16.4 introduced a monospaced(_ isActive:)
variant for Text
. If this were using @backDeployed
, the SwiftUI team might ensure the modifier is available to whatever earliest version of SwiftUI supports the implementation code they actually need, like this:
import SwiftUI
extension Text {
@backDeployed(before: iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4)
@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
public func monospaced(_ isActive: Bool) -> Text {
fatalError("Implementation here")
}
}
At runtime, Swift will use the system’s copy of SwiftUI if it has that modifier already, otherwise using the back-deployed version back to iOS 14.0 and similar.
SE-0365 takes another step towards letting us remove self
from closures by allowing an implicit self
in places where a weak self
capture has been unwrapped.
For example, in the code below we have a closure that captures self
weakly, but then unwraps self
immediately:
import Foundation
class TimerController {
var timer: Timer?
var fireCount = 0
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self else { return }
print("Timer has fired \(fireCount) times")
fireCount += 1
}
}
}
That code would not have compiled before Swift 5.8, because both instances of fireCount
in the closure would need to be written self.fireCount
.
SE-0274 adjusts the #file
magic identifier to use the format Module/Filename, e.g. MyApp/ContentView.swift. Previously, #file
contained the whole path to the Swift file, e.g. /Users/twostraws/Desktop/WhatsNewInSwift/WhatsNewInSwift/ContentView.swift
, which is unnecessarily long and also likely to contain things you’d rather not reveal.
Important: Right now this behavior is not enabled by default. SE-0362 adds a new -enable-upcoming-feature
compiler flag designed to let developers opt into new features before they are fully enabled in the language, so to enable the new #file
behavior you should add -enable-upcoming-feature ConciseMagicFile
to Other Swift Flags in Xcode.
If you’d like to have the old behavior after this flag is enabled, you should use #filePath
instead:
// New behavior, when enabled
print(#file)
// Old behavior, when needed
print(#filePath)
The Swift Evolution proposal for this change is worth reading because it mentions surprisingly large improvements in binary size and execution performance, and also for this quite brilliant paragraph explaining why having the full path is often a bad idea:
“The full path to a source file may contain a developer's username, hints about the configuration of a build farm, proprietary versions or identifiers, or the Sailor Scout you named an external disk after.”
SE-0375 extends a Swift 5.7 feature that allowed us to call generic functions using a protocol, fixing a small but annoying inconsistency: Swift 5.7 would not allow this behavior with optionals, whereas Swift 5.8 does.
For example, this code worked great in Swift 5.7, because it uses a non-optional T
parameter:
func double<T: Numeric>(_ number: T) -> T {
number * 2
}
let first = 1
let second = 2.0
let third: Float = 3
let numbers: [any Numeric] = [first, second, third]
for number in numbers {
print(double(number))
}
In Swift 5.8, that same parameter can now be optional, like this:
func optionalDouble<T: Numeric>(_ number: T?) -> T {
let numberToDouble = number ?? 0
return numberToDouble * 2
}
for number in numbers {
print(optionalDouble(number))
}
In Swift 5.7 that would have issued the rather baffling error message “Type 'any Numeric' cannot conform to 'Numeric’”, so it’s good to see this inconsistency resolved.
This resolves another small but potentially annoying inconsistency in Swift where downcasting a collection – e.g. casting an array of ClassA
to an array of another type that inherits from ClassA
– would not be allowed in some circumstances.
For example, this code is now valid in Swift 5.8, whereas it would not have worked previously:
class Pet { }
class Dog: Pet {
func bark() { print("Woof!") }
}
func bark(using pets: [Pet]) {
switch pets {
case let pets as [Dog]:
for pet in pets {
pet.bark()
}
default:
print("No barking today.")
}
}
Before Swift 5.8 that would have led to the error message, “Collection downcast in cast pattern is not implemented; use an explicit downcast to '[Dog]' instead.” In practice, syntax such as if let dogs = pets as? [Dog] {
worked just fine, so I would imagine that error was rarely seen. However, this change does mean another language inconsistency is resolved, which is always welcome.
if
and switch
expressionsSE-0380 adds the ability for us to use if
and switch
as expressions in several situations. This produces syntax that will be a little surprising at first, but overall it does help reduce a little extra syntax in the language.
As a simple example, we could set a variable to either “Pass” or “Fail” depending on a condition like this:
let score = 800
let simpleResult = if score > 500 { "Pass" } else { "Fail" }
print(simpleResult)
Or we could use a switch
expression to get a wider range of values like this:
let complexResult = switch score {
case ...300: "Fail"
case 301...500: "Pass"
case 501...800: "Merit"
default: "Distinction"
}
print(complexResult)
You don’t need to assign the result somewhere in order to use this new expression syntax, and in fact it combines beautifully with SE-0255 from Swift 5.1 that allows us to omit the return
keyword in single expression functions that return a value.
So, because both if
and switch
can now both be used as expressions, we can write a function like this one without using return
in all four possible cases:
func rating(for score: Int) -> String {
switch score {
case ...300: "Fail"
case 301...500: "Pass"
case 501...800: "Merit"
default: "Distinction"
}
}
print(rating(for: score))
You might be thinking this feature makes if
work more like the ternary conditional operator, and you’d be at least partly right. For example, we could have written our simple if
condition from earlier like this:
let ternaryResult = score > 500 ? "Pass" : "Fail"
print(ternaryResult)
However, the two are not identical, and there is one place in particular that might catch you out – you can see it in this code:
let customerRating = 4
let bonusMultiplier1 = customerRating > 3 ? 1.5 : 1
let bonusMultiplier2 = if customerRating > 3 { 1.5 } else { 1.0 }
Both those calculations produce a Double
with the value of 1.5, but pay attention to the alternative value for each of them: for the ternary option I’ve written 1, and for the if
expression I’ve written 1.0.
This is intentional: when using the ternary Swift checks the types of both values at the same time and so automatically considers 1 to be 1.0, whereas with the if
expression the two options are type checked independently: if we use 1.5 for one case and 1 for the other then we’ll be sending back a Double
and an Int
, which isn’t allowed.
SE-0393, SE-0398, and SE-0399 combined to form a rather dense knot of improvements to Swift that allow us to use variadic generics.
These proposals solve a significant problem in Swift, which is that generic functions required a specific number of type parameters. These functions could still accept variadic parameters, but they still had to use the same type ultimately.
As an example, we could have three different structs that represent different parts of our program:
struct FrontEndDev {
var name: String
}
struct BackEndDev {
var name: String
}
struct FullStackDev {
var name: String
}
In practice they would have lots more properties that make those types unique, but you get the point – three different types exist.
We could make instances of those structs like this:
let johnny = FrontEndDev(name: "Johnny Appleseed")
let jess = FrontEndDev(name: "Jessica Appleseed")
let kate = BackEndDev(name: "Kate Bell")
let kevin = BackEndDev(name: "Kevin Bell")
let derek = FullStackDev(name: "Derek Derekson")
And then when it came to actually doing work, we could pair developers together using a simple function like this one:
func pairUp1<T, U>(firstPeople: T..., secondPeople: U...) -> ([(T, U)]) {
assert(firstPeople.count == secondPeople.count, "You must provide equal numbers of people to pair.")
var result = [(T, U)]()
for i in 0..<firstPeople.count {
result.append((firstPeople[i], secondPeople[i]))
}
return result
}
That uses two variadic parameters to receive a group of first people and a group of second people, then returns them as an array.
We can now use that to create programmer pairs who can work on some back-end and front-end work together:
let result1 = pairUp1(firstPeople: johnny, jess, secondPeople: kate, kevin)
So far this is old, but here’s where things get interesting: Derek is a full-stack developer, and can therefore work as either a back-end developer or a front-end developer. However, if we tried to use johnny, derek
as the first parameter then Swift would refuse to build our code – it needs the types of all the first people and second people to be the same.
One way to fix this would be to throw away all our type information using Any
, but parameter packs allow us to solve this much more elegantly.
The syntax might be a little intense at first, so I’m going to show you the code then try to break it down. Here it is:
func pairUp2<each T, each U>(firstPeople: repeat each T, secondPeople: repeat each U) -> (repeat (first: each T, second: each U)) {
return (repeat (each firstPeople, each secondPeople))
}
There are four independent things happening there, so let’s work through them one by one:
<each T, each U>
creates two type parameter packs, T
and U
.repeat each T
is a pack expansion, which is what expands the parameter pack into actual values – it’s the equivalent of T...
, but avoids some confusion with ...
being used as an operator.T
and U
.return
keyword is what does the real work: it uses a pack expansion expression to take one value from T
and one from U
, putting them together into the returned value.What it doesn’t show is that the return type automatically ensures both our T
and U
types have the same shape – they have the same number of items inside them. So, rather than using assert()
like we had in the first function, Swift will simply issue a compiler error if we try to pass in two sets of data of different sizes.
With the new function in place, we can now pair up Derek with other developers, like this:
let result2 = pairUp2(firstPeople: johnny, derek, secondPeople: kate, kevin)
Now, what we’ve actually done is implement a simple zip()
function, which means we can write nonsense like this:
let result3 = pairUp2(firstPeople: johnny, derek, secondPeople: kate, 556)
That tries to pair Kevin with the number 556, which clearly doesn’t make any sense. This is where parameter packs really come into their own, because we could define protocols such as these:
protocol WritesFrontEndCode { }
protocol WritesBackEndCode { }
Then add some conformances:
FrontEndDev
should conform to WritesFrontEndCode
BackEndDev
should conform to WritesBackEndCode
FullStackDev
should conform to both WritesFrontEndCode
and WritesBackEndCode
And now we can add constraints to our type parameter packs:
func pairUp3<each T: WritesFrontEndCode, each U: WritesBackEndCode>(firstPeople: repeat each T, secondPeople: repeat each U) -> (repeat (first: each T, second: each U)) {
return (repeat (each firstPeople, each secondPeople))
}
That now means only sensible pairs can happen – we always get someone who can write front-end code paired with someone who can write back-end code, regardless of whether they are full-stack developers or not.
To transfer this over to something you’re more likely to be experienced with, we have a similar situation in SwiftUI. We regularly want to be able to create views with many subviews, and if we were working with a single view type such as Text
then you could imagine something like Text...
working great. But that wouldn’t work if we wanted to have some text, then an image, then a button, and more – any non-uniform layout would simply not be possible.
Trying to use AnyView...
or similar to erase the types throws away all the type information, so before Swift 5.9 this problem was solved by creating lots of function overloads. For example, SwiftUI’s view builder has buildBlock()
overloads that can combine two views, three views, four views, etc, all the way up to 10 views – but no further, because they need to draw a line somewhere.
SE-0382, SE-0389, and SE-0397 combine to add macros to Swift, which allow us to create code that transforms syntax at compile time.
Macros in something like C++ are a way to pre-process your code – to effectively perform text replacement on the code before it’s seen by the main compiler, so that you can generate code you really don’t want to write by hand.
Swift’s macros are similar, but significantly more powerful – and thus also significantly more complex. They also allow us to dynamically manipulate our project’s Swift code before it’s compiled, allowing us to inject extra functionality at compile time.
The key things to know are:
ExpressionMacro
to generate a single expression, AccessorMacro
to add getters and setters, and ConformanceMacro
to make a type conform to a protocol.That last part is particularly important: Swift’s macros support are built around Apple’s SwiftSyntax library for understanding and manipulating source code. You must add this as a dependency for your macros.
Let’s start with a simple macro, so you can see how they work. Because macros are run at compile time, we can make a tiny macro that returns the date and time our app was built – a helpful thing to have in your debug diagnostics. This takes several steps, several of which should take place a separate module from your main target.
First we need to create the code that performs the macro expansion – the thing that will turn #buildDate
into something like 2023-06-05T18:00:00Z:
public struct BuildDateMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
let date = ISO8601DateFormatter().string(from: .now)
return "\"\(raw: date)\""
}
}
Important: This code should not be in your main app target; we don’t want that code being compiled into our finished app, we just want the finished date string in there.
Inside that same module we create a struct that conforms to the CompilerPlugin
protocol, exporting our macro:
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
BuildDateMacro.self
]
}
We would then add that to our list of targets in Package.swift:
.macro(
name: "MyMacrosPlugin",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
That finishes creating the macro in an external module. The rest of our code takes place wherever we want to use the macro, such as in our main app target.
This takes two steps, starting with a definition of what the macro is. In our case this is a free-standing expression macro that will return a string, it exists inside the MyMacrosPlugin
module, and has the strict name BuildDateMacro
. So, we’d add this definition to our main target:
@freestanding(expression)
macro buildDate() -> String =
#externalMacro(module: "MyMacrosPlugin", type: "BuildDateMacro")
And the second step is to actually use the macro, like this:
print(#buildDate)
When you read through this code, the most important thing to take away is that the main macro functionality – all that code inside the BuildDateMacro
struct – is run at build time, with its results being injected back into the call sites. So, our little print()
call above would be rewritten to something like this:
print("2023-06-05T18:00:00Z")
This in turn means the code inside your macros can be as complex as you need: we could have crafted our date in any way we wanted, because all that finished code actually sees is the string we returned.
Now, in practice the Swift team recommends against this kind of macro, because they want us to build things with consistent output – they prefer macros that produce the same output given the same output, because it allows things like incremental builds to function efficiently.
Let’s try a slightly more useful macro, this time making a member attribute macro. When applied to a type such as a class, this lets us apply an attribute to every member in a class. This is identical in concept to the older @objcMembers
attribute, which adds @objc
to each of the properties in a type.
For example, if you have an observable object that uses @Published
on every one of its properties, you could write a simple @AllPublished
macro that does the job for you. First, write the macro itself:
public struct AllPublishedMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
[AttributeSyntax(attributeName: SimpleTypeIdentifierSyntax(name: .identifier("Published")))]
}
}
Second, include that in your list of provided macros:
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
BuildDateMacro.self,
AllPublishedMacro.self,
]
}
Third, declare the macro in your main app target, this time marking it as an attached member-attribute macro:
@attached(memberAttribute)
macro AllPublished() = #externalMacro(module: "MyMacrosPlugin", type: "AllPublishedMacro")
And now use it to annotate your observable object class:
@AllPublished class User: ObservableObject {
var username = "Taylor"
var age = 26
}
Our macros are able to accept parameters to control their behavior, although here it’s easy for the complexity to really shoot upwards. As an example, Doug Gregor from the Swift team maintains a small GitHub repository of example macros, including one neat one that checks hard-coded URLs are valid at build time – it becomes impossible to type a URL wrongly, because the build won’t proceed.
Declaring the macro in our app target is straightforward, including adding a string parameter:
@freestanding(expression) public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "MyMacrosPlugin", type: "URLMacro")
Using it is also straightforward:
let url = #URL("https://swift.org")
print(url.absoluteString)
That makes url
into a full URL
instance rather than an optional one, because we will have checked the URL is correct at compile time.
What’s harder is the actual macro itself, which needs to read the "https://swift.org" string that was passed in and convert it into a URL. Doug’s version is more thorough, but if we boil it down to the bare minimum we get this:
public struct URLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments
else {
fatalError("#URL requires a static string literal")
}
guard let _ = URL(string: segments.description) else {
fatalError("Malformed url: \(argument)")
}
return "URL(string: \(argument))!"
}
}
SwiftSyntax is marvelous, but it’s not what I’d call discoverable.
There are three more things I want to add before moving on.
First, the MacroExpansionContext
value we’re given has a very helpful makeUniqueName()
method, which will produce a new variable name that’s guaranteed not to conflict with any other names in the current context. If you’re looking to inject new names into the finished code, makeUniqueName()
is a smart move.
Second, one of the concerns with macros is the ability to debug your code when you hit a problem – it’s hard to trace what’s going on when you can’t actually step through code easily. Some work has already taken place inside SourceKit to expand macros as a refactoring operation, but really we need to see what ships in Xcode.
And finally, the extensive transformations that macros enable may mean that Swift Evolution itself will evolve over the next year or two, because so many features that might previously have required extensive compiler support and discussion can now be prototyped and perhaps even shipped using macros.
SE-0390 introduces the concept of structs and enums that cannot be copied, which in turn allows a single instance of a struct or enum to be shared in many places – they still ultimately have one owner, but can now be accessed in various parts of your code.
First, this change introduces new syntax to suppress a requirement: ~Copyable
. That means “this type cannot be copied”, and this suppression syntax is not available elsewhere at this time – we can’t use ~Equatable
, for example, to opt out of ==
for a type.
So, we could create a new noncopyable User
struct like this:
struct User: ~Copyable {
var name: String
}
Note: Noncopyable types cannot conform to any protocols other than Sendable
.
Once you create a User
instance, its noncopyable nature means that it’s used very differently from previous versions of Swift. For example, this kind of code might read like nothing special:
func createUser() {
let newUser = User(name: "Anonymous")
var userCopy = newUser
print(userCopy.name)
}
createUser()
But we’ve declared the User
struct as being noncopyable – how can that take a copy of newUser
? The answer is that it can’t: assigning newUser
to userCopy
causes the original newUser
value to be consumed, which means it can no longer be used because ownership now belongs to userCopy
. If you try changing print(userCopy.name)
to print(newUser.name)
you’ll see Swift throws up a compiler error – it’s just not allowed.
New restrictions also apply to how we use noncopyable types as function parameters: SE-0377 says that functions must specify whether they intend to consume the value and therefore render it invalid at the call site after the function finishes, or whether they want to borrow the value so that they can read all its data at the same time as other borrowing parts of our code.
So, we could write one function that creates a user, and another function that borrows the user to gain read-only access to its data:
func createAndGreetUser() {
let newUser = User(name: "Anonymous")
greet(newUser)
print("Goodbye, \(newUser.name)")
}
func greet(_ user: borrowing User) {
print("Hello, \(user.name)!")
}
createAndGreetUser()
In contrast, If we had made the greet()
function use consuming User
then the print("Goodbye, \(newUser.name)")
would not be allowed – Swift would consider the newUser
value to be invalid after greet()
has run. On the flip side, because consuming methods must end the lifetime of the object, they can mutate its properties freely.
This shared behavior gives noncopyable structs a superpower that was previously restricted to classes and actors: we can give them deinitializers that will automatically be run when the final reference to a noncopyable instance is destroyed. For example, this struct prints a message when it's destroyed:
struct Movie: ~Copyable {
var name: String
init(name: String) {
self.name = name
}
deinit {
print("\(name) is no longer available")
}
}
func watchMovie() {
let movie = Movie(name: "The Hunt for Red October")
print("Watching \(movie.name)")
}
watchMovie()
When that runs it prints “Watching The Hunt for Red October” then “The Hunt for Red October is no longer available”.
Methods inside a noncopyable type are borrowing by default, but they can be marked as mutating
just like copyable types, and they can also be marked as consuming to mean that the value is invalid after the method has been run.
As an example, you might know the movie and TV series Mission Impossible, where secret agents are given their mission instructions in a self-destructing tape that can be played only once. This is perfect for a consuming method like this:
struct MissionImpossibleMessage: ~Copyable {
private var message: String
init(message: String) {
self.message = message
}
consuming func read() {
print(message)
}
}
That marks the message itself as private, so it can only be access by calling the read()
method that consumes the instance.
Unlike mutating methods, consuming methods can be run on constant instances of your type. So, code like this is fine:
func createMessage() {
let message = MissionImpossibleMessage(message: "You need to abseil down a skyscraper for some reason.")
message.read()
}
createMessage()
Note: Because message.read()
consumes the message
instance, it is an error to attempt to call message.read()
a second time.
Consuming methods are made a little more complex when combined with deinitializers because they might double up on any clean up work you do. For example, if you were tracking high scores in a game you might want to have a consuming finalize()
method that writes the latest high score to permanent storage and stops anyone else from changing the score further, but you might also have a deinitializer that saves the latest score to disk when the object is destroyed.
To avoid this problem, Swift 5.9 introduces a new discard
operator that can be used on consuming methods of noncopyable types. When you use discard self
in a consuming method, it stop the deinitializer from being run for this object.
So, we could implement our HighScore
struct like this:
struct HighScore: ~Copyable {
var value = 0
consuming func finalize() {
print("Saving score to disk…")
discard self
}
deinit {
print("Deinit is saving score to disk…")
}
}
func createHighScore() {
var highScore = HighScore()
highScore.value = 20
highScore.finalize()
}
createHighScore()
There are a few extra complexities you need to be aware of when working with this new functionality:
Copyable
from existing types, because it dramatically changes how they are used. If you’re shipping code in a library, this will break your ABI.SE-0366 extends the concept of consuming values to local variables and constants of copyable types, which might benefit developers who want to avoid excess retain/release calls happening behind the scenes as their data is passed around.
In its simplest form, the consume
operator looks like this:
struct User {
var name: String
}
func createUser() {
let newUser = User(name: "Anonymous")
let userCopy = consume newUser
print(userCopy.name)
}
createUser()
The important line there is the let userCopy
line, which does two things at once:
newUser
into userCopy
.newUser
, so any further attempt to access it will throw up an error.This allows us to tell the compiler explicitly “do not allow me to use this value again,” and it will enforce the rule on our behalf.
I can see this being particularly common with the so-called black hole, _
, where we don’t want a copy of the data but simply want to mark it as being destroyed, like this:
func consumeUser() {
let newUser = User(name: "Anonymous")
_ = consume newUser
}
In practice, though, it’s possible the most common place the consume
operator will be used is when passing values into a function like this:
func createAndProcessUser() {
let newUser = User(name: "Anonymous")
process(user: consume newUser)
}
func process(user: User) {
print("Processing \(user.name)…")
}
createAndProcessUser()
There are two extra things I think are particularly worth knowing about this feature.
First, Swift tracks which branches of your code have consumed values, and enforces the rules conditionally. So, in this code only one of the two possibilities consumes our User
instance:
func greetRandomly() {
let user = User(name: "Taylor Swift")
if Bool.random() {
let userCopy = consume user
print("Hello, \(userCopy.name)")
} else {
print("Greetings, \(user.name)")
}
}
greetRandomly()
Second, technically speaking consume
operates on bindings not values. In practice this means if we consume using a variable, we can reinitialize the variable and use it just fine:
func createThenRecreate() {
var user = User(name: "Roy Kent")
_ = consume user
user = User(name: "Jamie Tartt")
print(user.name)
}
createThenRecreate()
SE-0388 adds a new makeStream()
method to both AsyncStream
and AsyncThrowingStream
that sends back both the stream itself alongside its continuation.
So, rather than writing code like this:
var _continuation: AsyncStream<String>.Continuation!
let stream = AsyncStream<String> { _continuation = $0 }
let continuation = _continuation!
We can now get both at the same time:
let (newStream, newContinuation) = AsyncStream.makeStream(of: String.self)
This is going to be particularly welcome in places where you need to access the continuation outside of the current context, such as in a different method. For example, previously we might have written a simple number generator like this one, which needs to store the continuation as its own property in order to be able to call it from the queueWork()
method:
struct OldNumberGenerator {
private var continuation: AsyncStream<Int>.Continuation!
var stream: AsyncStream<Int>!
init() {
stream = AsyncStream(Int.self) { continuation in
self.continuation = continuation
}
}
func queueWork() {
Task {
for i in 1...10 {
try await Task.sleep(for: .seconds(1))
continuation.yield(i)
}
continuation.finish()
}
}
}
With the new makeStream(of:)
method this code becomes much simpler:
struct NewNumberGenerator {
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
func queueWork() {
Task {
for i in 1...10 {
try await Task.sleep(for: .seconds(1))
continuation.yield(i)
}
continuation.finish()
}
}
}
SE-0374 adds a new extension method to Swift’s Clock
protocol that allows us to suspend execution for a set number of seconds, but also extends duration-based Task
sleeping to support a specific tolerance.
The Clock
change is a small but important one, particularly if you’re mocking a concrete Clock
instance to remove delays in tests that would otherwise exist in production.
For example, this class can be created with any kind of Clock
, and will sleep using that clock before triggering a save operation:
import Foundation
class DataController: ObservableObject {
var clock: any Clock<Duration>
init(clock: any Clock<Duration>) {
self.clock = clock
}
func delayedSave() async throws {
try await clock.sleep(for: .seconds(1))
print("Saving…")
}
}
Because that uses any Clock<Duration>
, it’s now possible to use something like ContinuousClock
in production but your own DummyClock
in testing, where you ignore all sleep()
commands to keep your tests running quickly.
In older versions of Swift the equivalent code would in theory have been try await clock.sleep(until: clock.now.advanced(by: .seconds(1)))
, but that wouldn’t work in this example because clock.now
isn’t available as Swift doesn’t know exactly what kind of clock has been used.
As for the change to Task
sleeping, it means we can go from code like this:
try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5))
To just this:
try await Task.sleep(for: .seconds(1), tolerance: .seconds(0.5))
SE-0381 adds new discardable task groups that fix an important gap in the current API: tasks that are created inside a task group are automatically discarded and destroyed as soon as they finish, which means task groups that run for extended periods of time (or perhaps forever, as in the case of a web server) won’t leak memory over time.
When using the original withTaskGroup()
API, a problem can occurs because of the way Swift only discards a child task and its resulting data when we call next()
or loop over the task group’s children. Calling next()
will cause your code to suspend if all child tasks are currently executing, so we hit the problem: you want a server that’s always listening for connections so you can add tasks to process them, but you also need to stop every so often to clean up old tasks that have completed.
There was no clean solution to this until Swift 5.9, which adds withDiscardingTaskGroup()
and withThrowingDiscardingTaskGroup()
functions that create new discarding task groups. These are task groups that automatically discard and destroy each task as soon as it completes, without us needing to call next()
to consume it manually.
To give you an idea of what triggers the problem, we could implement a naive directory watcher that loops forever and reports back the names of any files or directories that have been added or removed:
import Foundation
struct FileWatcher {
// The URL we're watching for file changes.
let url: URL
// The set of URLs we've already returned.
private var handled = Set<URL>()
init(url: URL) {
self.url = url
}
mutating func next() async throws -> URL? {
while true {
// Read the latest contents of our directory, or exit if a problem occurred.
guard let contents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else {
return nil
}
// Figure out which URLs we haven't already handled.
let unhandled = handled.symmetricDifference(contents)
if let newURL = unhandled.first {
// If we already handled this URL then it must be deleted.
if handled.contains(newURL) {
handled.remove(newURL)
} else {
// Otherwise this URL is new, so mark it as handled.
handled.insert(newURL)
return newURL
}
} else {
// No file difference; sleep for a few seconds then try again.
try await Task.sleep(for: .microseconds(1000))
}
}
}
}
We could then use that from inside a simple app, although for brevity we’ll just print the URLs rather than do any actual complicated processing:
struct FileProcessor {
static func main() async throws {
var watcher = FileWatcher(url: URL(filePath: "/Users/twostraws"))
try await withThrowingTaskGroup(of: Void.self) { group in
while let newURL = try await watcher.next() {
group.addTask {
process(newURL)
}
}
}
}
static func process(_ url: URL) {
print("Processing \(url.path())")
}
}
That will run forever, or at least until either the user terminates the program or the directory we’re watching stops being accessible. However, because it uses withThrowingTaskGroup()
it has a problem: a new child task is created every time addTask()
is called, but because it doesn’t call group.next()
anywhere those child tasks are never destroyed. Little by little – maybe only a few hundred bytes each time – this code will eat more and more memory until eventually the operating system runs out of RAM and is forced to terminate the program.
This problem goes away entirely with discarding task groups: just replacing withThrowingTaskGroup(of: Void.self)
with withThrowingDiscardingTaskGroup
means each child task is automatically destroyed as soon as its work finishes.
In practice, this problem is mainly going to be faced by server code, where the server must be able to accept new connections while handling existing ones smoothly.
Swift concurrency was introduced back in Swift 5.5, but had a bit of a rocky adoption both in Apple's own frameworks and our own projects. However, with Swift 5.10 the team made a rather dramatic statement: "Swift 5.10 closes all known static data-race safety holes in complete strict concurrency checking."
Concurrency checking is what allows the compiler to verify our use of concurrent code is safe – that we aren't accidentally sharing mutable state in a way that can cause race conditions. Of course, the key word here is "known": everything they know about has been resolved.
Apple's work here is not only hugely innovative, but hugely complex: similar to how type inference requires the Swift compiler to be able to reason about how various parts of our code are used, in concurrency the compiler is effectively running a series of algorithms that attempt to determine conclusively that our code is concurrency-safe.
To give you a concrete example, this code generated a warning in Swift 5.9:
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Tap Me", action: doWork)
}
func doWork() {
print("Hello")
}
}
That would throw up the rather unhelpful warning, "Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor MainActor".
The problem here is that SwiftUI's Button
view doesn't use @MainActor
for its action, so Swift was throwing up a warning that we were calling a main actor-method from somewhere that isn't isolated to the main actor. This warning has been removed by the concurrency checking improvements in Swift 5.10: the compiler can now see the button action exists in side the body
property, which is isolated to the main actor, and therefore is safe.
SE-0404 allows us to create nested protocols, meaning that we can place protocols inside structs, enums, classes, actors, and even functions, with the sole restriction that whatever we're nesting the protocol in can't use generics.
This is particularly helpful when common names are given to protocols. For example, the word "transaction" could feasibly be used to mean an animation transaction, a bank transaction, and a database transaction all in the same app.
One way to resolve this is by using compound names – we add more words to protocol names to clarify what we mean, like this:
protocol AnimationTransaction {
}
protocol BankTransaction {
}
protocol DatabaseTransaction {
}
Another common problem also occurs when several similar protocols exist. For example, in SwiftUI we have protocols for ButtonStyle
, LabelStyle
, ListStyle
, and more, all encapsulating the idea that a view can be styled in various ways.
Both of these can be resolved with this change. In the case of transactions, we could nest each transaction type inside whatever type it operated on:
struct Animation {
protocol Transaction {
}
}
struct Bank {
protocol Transaction {
}
}
struct Database {
protocol Transaction {
}
}
Where those protocols are used externally, they would now be written Animation.Transaction
, Bank.Transaction
, and Database.Transaction
, but inside their respective structs they can just be referred to as Transaction
.
In theory, SwiftUI could also move to Button.Style
, List.Style
, and so on, but that feels like a big change at this point.
SE-0383 formally deprecates the @UIApplicationMain
and @NSApplicationMain
attributes, encouraging folks to switch across to the general-purpose @main
attribute that was introduced back in Swift 5.3.
Adopting this change is trivial. You should be able to change this code:
import SwiftUI
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// your code here
}
To this:
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// your code here
}
Or if you're using SwiftUI, just this:
@main
struct SandboxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
SE-0327 adds some clarifications about how state inside actors is created and destroyed, and also relaxes some rules that were overly restrictive.
This proposal contains a number of small, specific changes to actors. For example, Swift will now automatically make actors with an async initializer move to the actor's executor when all its properties are initialized.
In code, it means the two print()
calls shown below might execute on entirely different threads:
actor Actor {
var name: String
init(name: String) async {
print(name)
self.name = name
print(name)
}
}
let actor = await Actor(name: "Margot")
This means the code has a potential suspension directly after the name
property is set.
Swift 6 contains another barrage of updates around concurrency, and the team ought to be proud of the extraordinary advances they have made to make this release possible.
By far the biggest change is that complete concurrency checking is enabled by default. Unless you're very fortunate indeed, there's a very good chance your code will need some adjustment – it's no surprise the Swift team made it optional in earlier versions to give folks time to evaluate what's changing.
Swift 6 improves concurrency checking further, and the Swift team say it "removes many false-positive data-race warnings" that were present in 5.10. It also introduces several targeted changes that will do wonders to make concurrency easier to adopt – if you tried with 5.10 and found things just too gnarly to figure out, hopefully some of the changes in Swift 6 will help.
Easily the biggest is SE-0414, defines isolation regions that allow the compiler to conclusively prove different parts of your code can run concurrently.
At the core of this change lies the existing concept of sendability. A Sendable
type is one that can be safely passed around in a concurrent environment, which can include value types such as structs, final classes with constant properties, actors that automatically protect their own mutable state, and more.
Before Swift 6 the compiler was very strict: if you had a non-sendable value on one actor and tried to send it to another actor, you'd get concurrency checking warnings. For example, although SwiftUI view bodies run on the main actor, SwiftUI views themselves don't, which can easily cause all sorts of false positive warnings from the compiler – Swift thinks there's a potential race condition when really there isn't.
You can see the problem with the following code:
class User {
var name = "Anonymous"
}
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.task {
let user = User()
await loadData(for: user)
}
}
func loadData(for user: User) async {
print("Loading data for \(user.name)…")
}
}
Before Swift 6 the call to loadData()
would throw up a warning: "passing argument of non-sendable type 'User' outside of main actor-isolated context may introduce data races."
After Swift 6 this warning goes away: Swift now detects that the code doesn't actually present a problem because user
isn't being accessed from two or more places at once, so it won't emit a warning – the compiler is able to analyze the program's flow and detect that it's safe.
This change effectively means sendable objects are now either those that conform to Sendable
, or those that don't need to conform to Sendable
because the compiler can prove they are being used safely – it's a dramatic simplification of concurrency for developers, made possible by truly cutting-edge compiler development.
But there are many other, smaller improvements, including:
sending
keyword for when we need to send values between isolation regions.async
functions that are isolated to the same actor as their caller.Some other changes were present in earlier versions of Swift, but hidden behind feature flags. For example, SE-0401 removes a feature that was introduced back in Swift 5.5: actor inference for property wrappers.
Previously, any struct or class using a property wrapper with @MainActor
for its wrapped value will automatically be @MainActor
. This is what makes @StateObject
and @ObservedObject
convey "main-actor-ness" on SwiftUI views that use them – if you use either of those two property wrappers in a SwiftUI view, the whole view becomes @MainActor
too.
As an example, consider the view model below, marked with @MainActor
as is good practice:
@MainActor
class ViewModel: ObservableObject {
func authenticate() {
print("Authenticating…")
}
}
If you want to use that from a SwiftUI view using @StateObject
, you must also mark the view with @MainActor
from Swift 6 and later, like this:
@MainActor
struct LogInView: View {
@StateObject private var model = ViewModel()
var body: some View {
Button("Hello, world", action: startAuthentication)
}
func startAuthentication() {
model.authenticate()
}
}
Before Swift 6, @MainActor
would have been conferred on the whole view because of its @StateObject
property.
Another old changed that's now enable in Swift 6 is SE-0412 requires global variables to be safe in concurrent environments.
This applies to loose variables you might have in your projects at global scope:
var gigawatts = 1.21
But also to static variables stored in types:
struct House {
static var motto = "Winter is coming"
}
This data can be accessed anywhere at any time, which makes it inherently unsafe. To resolve the problem you either need to convert the variable into a sendable constant, restrict it to a global actor, e.g. @MainActor
, or, if you have no other option or know it's protected somewhere else, mark it nonisolated.
For example, all of these are allowed:
struct XWing {
@MainActor
static var sFoilsAttackPosition = true
}
struct WarpDrive {
static let maximumSpeed = 9.975
}
@MainActor
var idNumber = 24601
// Not recommended unless you're certain it's safe
nonisolated(unsafe) var britishCandy = ["Kit Kat", "Mars Bar", "Skittles", "Starburst", "Twix"]
Another feature present earlier but now enabled is SE-0411, which changes function default values to have the same isolation as the function they are inside.
For example, the code below is now allowed, when previously it would have triggered an error:
@MainActor
class Logger {
}
@MainActor
class DataController {
init(logger: Logger = Logger()) {
}
}
Because both DataController
and Logger
have been restricted to the main actor, Swift now considers the Logger()
creation to also be restricted to the main actor, which makes perfect sense.
Swift concurrency remains a bit of a moving target, but if you'd like to know more I highly recommend Matt Massicotte's blog – I don't think anyone is doing more to educate Swift developers about effective adoption of Swift concurrency.
And remember: if Swift 6 throws up concurrency warnings and errors about your code, those problems were there beforehand too – they just weren't being diagnosed automatically!
SE-0220 introduced a new count(where:)
method that performs the equivalent of a filter()
and count in a single pass. This saves the creation of a new array that gets immediately discarded, and provides a clear and concise solution to a common problem.
This example creates an array of test results, and counts how many are greater or equal to 85:
let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }
And this counts how many names in an array start with “Terry”:
let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }
This method is available to all types that conform to Sequence
, so you can use it on sets and dictionaries too.
Note: count(where:)
was originally planned for Swift 5.0 way back in 2019, but was withdrawn at the time for performance reasons.
SE-0413 introduced the ability to specify exactly what types of errors a function can throw, known as "typed throws". This resolves an annoyance with errors in Swift: we needed a general catch clause even when we had specifically caught all possible errors.
As an example of typed throws, we could define a CopierError
that can track when a photocopier runs out of paper:
enum CopierError: Error {
case outOfPaper
}
We could then create a Photocopier
struct that creates some number of copies of a page. This might throw errors if there isn't enough paper loaded for the requested operation, but rather than mark it simply as throws
we'll use throws(CopierError)
to be clear exactly what kind of errors can be thrown:
struct Photocopier {
var pagesRemaining: Int
mutating func copy(count: Int) throws(CopierError) {
guard count >= pagesRemaining else {
throw CopierError.outOfPaper
}
pagesRemaining -= count
}
}
Note: With this change you can either use throws
to specify any kind of error being thrown, or throws(OneSpecificErrorType)
to signal that only that one type can be thrown. You cannot write throws(A, B, C)
to throw one of several errors.
Now we can write code to attempt photocopying, catching the single error that can possibly be thrown:
do {
var copier = Photocopier(pagesRemaining: 100)
try copier.copy(count: 10)
} catch CopierError.outOfPaper {
print("Please refill the paper")
}
That call site is the important change here: in earlier versions of Swift we'd need a so-called "Pokémon catch" at the end, because Swift couldn't be sure exactly error types could be thrown – you've "gotta catch 'em all."
This comes with several other advantages:
CopierError
is the only error type that can be thrown, we can write throw .outOfPaper
.do
block only throws one kind of error, the error
value in a general catch
block will automatically have the same error type rather than being any kind of error.throws
clause, Swift will issue a compile error.Where this gets really clever is that throws(any Error)
is equivalent to using just throws
by itself, and throws(Never)
is equivalent to a non-throwing function. That might sound obscure, but it means in many places rethrows
can be expressed more clearly: the function throws whatever the function parameter throws.
As an example, Swift 6's new count(where:)
method accepts a closure used to evaluate how many items match whatever kind of filter you're running. That closure might throw errors, and if it does count(where:)
will throw that same error type:
public func count<E>(
where predicate: (Element) throws(E) -> Bool
) throws(E) -> Int {
print("Code goes here")
return 0
}
If that closure doesn't throw an error, throws(E)
is effectively throws(Never)
, meaning that count(where:)
will also not throw errors.
Even though typed throws seem very appealing, they are aren't a great choice when the errors that can be thrown might change in the future. They are a particularly poor choice in library code, because they lock you into a contract you might not want to stick to in the future.
In fact, here I'll just defer to the authors of the evolution proposal, who sum it up like this: even with the addition of typed throws to Swift, untyped throws is better for most scenarios.
Where typed throws are particularly useful is in the increasingly important realm of embedded Swift, where performance and predictability is critical.
SE-0408 introduces pack iteration, which adds the ability to loop over the parameter pack feature introduced in Swift 5.9.
Although value packs remain one of the most complex features of Swift, the evolution proposal shows just how useful this feature is by adding tuple comparison for any arity in just a few lines of code:
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
for (left, right) in repeat (each lhs, each rhs) {
guard left == right else { return false }
}
return true
}
If that means nothing to you, the Simple English version is that SE-0015 added support for direct tuple comparison up to arity 6, meaning that two tuples with up to six items could be compared using ==. If you tried comparing tuples with seven items – e.g. (1, 2, 3, 4, 5, 6, 7) == (1, 2, 3, 4, 5, 6, 7)
– Swift would throw up an error. SE-0408, along with the code above, removes that restriction.
Tantalizingly, the Future Directions section of this evolution proposal suggest that in the future we might see a variant of Swift's zip()
function that supports any number of sequences.
SE-0270 introduces various new methods to handle more complex operations on collections, such as moving or remove multiple items that aren't contiguous.
This change is powered by a new type called RangeSet
. If you've ever used IndexSet
from Foundation, think of RangeSet
as being IndexSet
except for any kind of Comparable
type rather than just integers.
Lots of Swift API has been upgraded to RangeSet
. To give us some example data to work with, we could create an array of students with exam results like this:
struct ExamResult {
var student: String
var score: Int
}
let results = [
ExamResult(student: "Eric Effiong", score: 95),
ExamResult(student: "Maeve Wiley", score: 70),
ExamResult(student: "Otis Milburn", score: 100)
]
We can get a RangeSet
containing the indices of all students who score 85% or higher like this:
let topResults = results.indices { student in
student.score >= 85
}
And if we wanted to get access to those students, we can use a new Collection
subscript:
for result in results[topResults] {
print("\(result.student) scored \(result.score)%")
}
This subscript returns another new type called DiscontiguousSlice
, which is similar to Slice
in that for performance reasons it refers to elements stored in a different collection, except the indices are discontiguous, meaning that they aren't necessarily adjacent in the collection.
The "set" part of the name is there because RangeSet
supports a variety of functions that come from the SetAlgebra
protocol, including union()
, intersection()
, and isSuperset(of:)
. This also means that inserting one range into another will merge any overlapping ranges rather than creating duplicates.
SE-0409 adds the ability to mark import declarations with access control modifiers, such as private import SomeLibrary
.
There are various ways this will be useful, including the ability for library developers to avoid accidentally leaking their own dependencies. For example, a banking might be split into multiple parts:
So, the app depends on the Banking library, and the Banking library in turn depends on Transactions, Networking, and other internal libraries.
We can demonstrate that setup with some code that also demonstrates the problem being resolved here. First, we could say that the low-level Transactions package has a struct such as this one:
public struct BankTransaction {
// code here
}
Up in the Banking library we might write a function to send money from one account number to another using that BankTransaction
:
public func sendMoney(from: Int, to: Int) -> BankTransaction {
// handle sending money then send back the result
return BankTransaction()
}
And now in the main app we can call sendMoney()
to do the work.
That's all regular Swift code, but it can create a rather unpleasant problem: very often wrapper libraries don't want to reveal the inner workings of the libraries they rely on internally, which is exactly what happens here – our main app is given access to the BankTransaction
struct from the Transactions library, when really it should only use APIs from the Banking library.
From 6.0 onwards we can solve this problem by using access control on the import for Transactions: by using internal import Transactions
or similar in the Banking library, Swift will refuse to build any code declared as public that exposes API from the Transactions library.
This really helps to clear up code boundaries: the Banking framework can still go ahead and use all the libraries it wants internally, but it won't be allowed to send those back to clients – the app in this case – by accident. If we genuinely did want to expose the internal framework types, we would use public import Transactions
to make that explicit.
On a more fine-grained level, this also allows files inside the same module to add extra restrictions – one file could privately import a framework without wanting to accidentally expose the contents of that framework elsewhere.
Although Swift 6 hasn't shipped yet, it's looking like the default for imports will be internal
when running in Swift 6 mode, but public
in Swift 5 mode to retain compatibility with existing code.
Noncopyable types were introduced in Swift 5.9, but are getting several upgrades in Swift 6.
As a reminder, noncopyable types allow us create types that have unique ownership, which we can pass around using borrowing or consuming as needed.
One example of noncopyable types I previously used were the secret messages used in the Mission Impossible movies – they famously self-destruct after being read, which we can model with a noncopyable type that is consumed (i.e. destroyed) upon reading:
struct Message: ~Copyable {
var agent: String
private var message: String
init(agent: String, message: String) {
self.agent = agent
self.message = message
}
consuming func read() {
print("\(agent): \(message)")
}
}
func createMessage() {
let message = Message(agent: "Ethan Hunt", message: "You need to abseil down a skyscraper for some reason.")
message.read()
}
createMessage()
In that code, the compiler enforces that message.read()
can only ever be called once, because it consumes the object.
The first major improvement is SE-0427, which introduces a batch of improvements at once. The biggest of those is that every struct, class, enum, generic type parameter, and protocol in Swift 6 automatically conforms to a new Copyable
protocol unless you explicitly opt out using ~Copyable
.
This impacts on the other changes introduced with this proposal. For example, noncopyable types can now be used with generics, allowing things like optional noncopyable instances because Swift's Optional
is implemented a generic enum. However, because generic type parameters automatically conform to Copyable
we must explicitly opt out using ~Copyable
.
Similarly, this change means noncopyable types can now conform to protocols, but only when those protocols are also marked ~Copyable
because otherwise they get automatically opted into Copyable
as mentioned above. (In case you were curious, Copyable
types can conform to noncopyable protocols just fine.)
SE-0429 improves things further by adding partial consumption of noncopyable values.
Previously it could be a problem when one noncopyable type incorporated another. For example, even fairly trivial code like the below was a problem before SE-0429:
struct Package: ~Copyable {
var from: String = "IMF"
var message: Message
consuming func read() {
message.read()
}
}
That code is now valid Swift, as long as the types in question don't have deinitializers.
A third major noncopyable improvement is SE-0432, which allows us to borrow noncopyable types while switching over them. Previously it was impossible to do pattern matching with where
clauses that depended on noncopyable values, whereas thanks to SE-0432 this is now possible in Swift 6.
Continuing our Mission Impossible example, we could say that one set of orders might be signed or anonymous, like this:
enum ImpossibleOrder: ~Copyable {
case signed(Package)
case anonymous(Message)
}
Because that enum has associated values that are noncopyable, it must itself be noncopyable. However, the associated values being noncopyable also means that pattern matching with where
was tricky – if you wanted to perform one set of actions for one Message
type, and a different set for another Message
type, you were out of luck.
With SE-0432 this is now resolved, meaning code like the below is now allowed:
func issueOrders() {
let message = Message(agent: "Ethan Hunt", message: "You need to abseil down a skyscraper for some reason.")
let order = ImpossibleOrder.anonymous(message)
switch consume order {
case .signed(let package):
package.read()
case .anonymous(let message) where message.agent == "Ethan Hunt":
print("Play dramatic music")
message.read()
case .anonymous(let message):
message.read()
}
}
Put together, this collection of changes helps make noncopyable types work much more naturally in Swift.
SE-0425 introduces Int128
and UInt128
. I literally have nothing more to say about these, because I think you already know exactly how they work – even the evolution proposal says, "the actual API of the types is uninteresting."
Still, I'd feel guilty if I didn't at least give you a code sample, so here goes:
let enoughForAnybody: Int128 = 170_141_183_460_469_231_731_687_303_715_884_105_727
SE-0426 introduces a new BitwiseCopyable
protocol, which has the sole purpose of allowing the compiler to create more optimized code for conforming types.
Most of the time you don't need to do anything to enable BitwiseCopyable
support. Swift will automatically apply it to most structs and enums you create as long as all the properties they contain are also bitwise copyable. That includes a huge collection of built-in types: all integers, all floating-point numbers, Bool
, Duration
, StaticString
, and more.
Where things take a little more thinking is when you're building a library – if Swift were to automatically apply a conformance to BitwiseCopyable
it could cause problems if your type changed in the future in a way that made it not support the protocol.
So, Swift disables the automatic inference for types you export with public
or package
visibility unless you explicitly mark those types with @frozen
.
If you specifically need to disable BitwiseCopyable
, you can do that by adding ~BitwiseCopyable
to your type's inheritance list. For example, the standard library's CommandLine
enum is both public
and @frozen
, so the Swift team explicitly opt out of it being bitwise copyable like this:
@frozen
public enum CommandLine : ~BitwiseCopyable {
}
Important: Opting out of BitwiseCopyable
must happen directly where your type is declared rather than in an extension.
No results – perhaps you should broaden your filter.
What's New In Swift was written by Paul Hudson. If you have questions or comments you can tweet me @twostraws or email paul@hackingwithswift.com.
This site is specifically about the Swift language. If you want to learn SwiftUI you should check my list of free SwiftUI tutorials or my free online book SwiftUI By Example.
A lot of the content for this site was drawn from previous articles I've written:
If you're just learning Swift, Hacking with Swift has many free Swift tutorials – check them out!