UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

What’s new in Swift 5.6?

Type placeholders, unavailable checks, Codable improvements, and more.

Paul Hudson       @twostraws

Swift 5.6 introduces another barrage of new features to the language, while refining others as we get closer to Swift 6. 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.

Hacking with Swift is sponsored by Proxyman

SPONSORED Proxyman: A high-performance, native macOS app for developers to easily capture, inspect, and manipulate HTTP/HTTPS traffic. The ultimate tool for debugging network traffic, supporting both iOS and Android simulators and physical devices.

Start for free

Sponsor Hacking with Swift and reach the world's largest Swift community!

Introduce existential 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.”

Type placeholders

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.

Allow coding of non 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.

Unavailability condition

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.

More concurrency changes

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, although @Sendable remains one of the more tricky areas of Swift.

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

Using @MainActor with @StateObject was specifically recommended at WWDC21, so it’s disappointing to see that the only correct code now is using an initializer that Apple’s own documentation says to avoid.

Given that more concurrency changes are clearly en route before Swift 6, I hope this all gets cleaned up in a way that feels less like we’re swimming against the current.

Plugins for Swift Package Manager

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.

This definitely feels like an area that will grow very quickly in future releases.

One step closer to Swift 6

Although we still haven’t the first clue when Swift 6 will land (WWDC23, anyone?) it does increasingly seem to be Apple’s big chance to break a number of things at the same time – the chance to go back and clean up things that weren’t quite fully baked in time for Swift 3, or weren’t even possible until the major changes introduced in 5.1 and 5.5.

So, be in no doubt: Swift 6 is very likely to break your code. That’s not a bad thing, and certainly Apple’s slow and steady migration path will make the process significantly easier than the move to Swift 3. However, it does mean we’re probably faced with another few years of many tutorials being out of date, best practice still being up in the air, and some degree of code churn.

What’s your favorite feature of Swift 5.6? Let me know on Twitter!

Hacking with Swift is sponsored by Proxyman

SPONSORED Proxyman: A high-performance, native macOS app for developers to easily capture, inspect, and manipulate HTTP/HTTPS traffic. The ultimate tool for debugging network traffic, supporting both iOS and Android simulators and physical devices.

Start for free

Sponsor Hacking with Swift and reach the world's largest Swift community!

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.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.