BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

What’s new in Swift 5.7

Or as I’ve started calling it, what isn’t new in Swift 5.7?

Paul Hudson       @twostraws

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.

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

if let shorthand for unwrapping optionals

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)!")
}

Multi-statement closure type inference

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"
    }
}

Clock, Instant, and Duration

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:

  • Clocks represent a way of measuring time passing. There are two built in: the continuous clock keeps incrementing time even when the system is asleep, and the suspending clock does not.
  • Instants represent an exact moment in time.
  • Durations represent how much time elapsed between two instants.

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")

Regular expressions

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

  • SE-0350 introduces a new Regex type
  • SE-0351 introduces a result builder-powered DSL for creating regular expressions.
  • SE-0354 adds the ability co create a regular expression using /.../ rather than going through Regex and a string.
  • SE-0357 adds many new string processing algorithms based on regular expressions.

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:

  • In that first regular expression we’re asking for the range of all substrings that match any lowercase alphabetic letter followed by “at”, so that would find the locations of “cat”, “sat”, and “mat”.
  • In the second one we’re matching the range “a” through “m” only, so it will print “the dog sat on the dog”.
  • In the third one we’re looking for “The”, but I’ve modified the regex to be case insensitive so that it matches “the”, “THE”, and so on.

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.

Type inference from default expressions

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())

Concurrency in top-level code

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.

Opaque parameter declarations

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.

Structural opaque result types

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.

Unlock existentials for all protocols

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 }
}

Lightweight same-type requirements for primary associated types

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.

Constrained existential types

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!

Distributed actor isolation

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:

  1. Swift’s approach of location transparency effectively forces us to assume the actors are remote, and in fact provides no way of determining at compile time whether an actor is local or remote – we just use the same 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.
  2. Rather than forcing us to build our own actor transport systems, Apple is providing a ready-made implementation for us to use. Apple has said they “only expect a handful of mature implementations to take the stage eventually,” but helpfully all the distributed actor features in Swift are agnostic of whatever actor transport you use.
  3. To move from an actor to a distributed actor we mostly just need to write 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.

buildPartialBlock for result builders

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.

Implicitly opened existentials

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.

Unavailable from async attribute

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.

But wait… there’s more!

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…

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.