Or as I’ve started calling it, what isn’t new in Swift 5.7?
Swift 5.7 introduces another gigantic collection of changes and improvements to the language, including power features such as regular expressions, quality of life improvements like the if let
shorthand syntax, and a great deal of consistency clean ups around the any
and some
keywords.
In this article I want to introduce you to the major changes, providing some hands-on examples along the way so you can see for yourself what’s changing. Many of these changes are complex, and many of them are also interlinked. I’ve done my best to break things down into a sensible order and provide hands-on explanations, but it took a huge amount of work so don’t be surprised to spot mistakes – if you find any, please send me a tweet and I’ll get it fixed!
I’m grateful to Holly Borla for taking the time to answer questions from me regarding the new generics proposals – if any mistakes crept through, they are mine and not hers.
Tip: You can also download this as an Xcode playground if you want to try the code samples yourself.
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
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 protocols more freely, as long as we don’t do anything that specifically needs to know 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.
At this point I expect your head is spinning with all the changes, but there are more I haven’t even touched:
It’s pretty clear there are a vast number of changes happening, some of which will actually break projects. So, to avoid causing too much disruption, the Swift team have decided to delay enabling some of these changes until Swift 6 lands.
It's not easy to predict when Swift 6 will arrive, but I would expect Swift 5.8 to arrive early in 2023 with Swift 6.0 perhaps arriving as early as WWDC23. If that happens, it would give Apple the chance to explain the variety of code-breaking changes in more detail, because it really does look like it's going to hit hard…
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.