Skip to main content

What’s New in Swift 5.6

· 8 min read
Nikita Lazarev-Zubov

What’s New in Swift 5.6

Introduction

Since 2014, the year Swift was born and included in Xcode, the language has matured and become an integral part of software development on Apple platforms. The days when Swift language syntax would change with every major release are gone, but Swift still can surprise. Judge for yourself: Synthesized conformance to Equatable was introduced in Swift 4.1, built-in randomization functionality in Swift 4.2, the Result type in Swift 5, support for Windows in Swift 5.3, and so on.

Built-in concurrency—released just last September with Swift 5.5—became an exciting game changer. And now, the next language release, 5.6, is out. In this post, we’ll take a peek at what this latest version delivers.

Concurrency Evolution

After introducing so many thrilling concurrency-related features in Swift 5.5, it‘s not a surprise to see more of this in 5.6.

Incremental Migration to Concurrency

Swift 6 is expected to be packed with breaking concurrency changes, but until that happens, we now have a chance to migrate to the new concurrency model incrementally. If you want to migrate your libraries to built-in concurrency, you may want to first start annotating your closures with@Sendable. This annotation marks values as safe to be used in Swift's modern concurrency environment, or as Swift language developers put it, the values are “data race safe.” Passing a “non-sendable” value to concurrent code generates a warning (given the “-warn-concurrency” compiler option is enabled):

    func runConcurrently(_ task: @Sendable () -> Void) { }

class OldStyleClass {
var message = "Sent"
func callFromOldStyleCode() {
runConcurrently {
message = "Received" // "​​Capture of 'self' with non-sendable type 'OldStyleClass' in a `@Sendable` closure"
}
}
}

If you annotate runConcurrently(_:) with @preconcurrency, the warning will only be generated in a new concurrency environment; this is because the "@preconcurrency" annotation disables the warning for code that doesn't use modern Swift concurrency features:

    @preconcurrency func runConcurrently(_ task: @Sendable () -> Void) { }

class OldStyleClass {
var message = "Sent"
func callFromOldStyleCode() {
runConcurrently {
message = "Received" // No warnings.
}
}
}

class NewStyleClass {
var message = "Sent"
func callFromNewStyleCode() async {
runConcurrently {
message = "Received" // The same warning.
}
}
}

Above, the method implementation inside the NewStyleClass has the async keyword. This means it takes advantage of the modern Swift concurrency, whereas the method inside the OldStyleClass doesn’t and is thus affected by the warning.

You can also use @preconcurrency to annotate imports of old modules to suppress such warnings when passing a module’s types to concurrency environments.

Actors Isolation Warnings

Global actors instantiated as default values of instance-member properties are now outlawed and generate a warning:

    @MainActor struct Dependency { }

class Client {
@MainActor let member = Dependency() // Expression requiring global actor 'MainActor' cannot appear in default-value expression of property 'member'; this is an error in Swift 6
}

As you can see, the warning will become an error in Swift 6, so it’s better to prepare your code now. To get rid of the warning, just move the initialization to the init block:

    @MainActor struct Dependency { }

class Client {
let member: Dependency
@MainActor init() {
member = Dependency()
}
}

Syntax Additions

any Keyword

Swift 5.6 introduces a new keyword for using protocols as types in the form of any:

    protocol Employee { }
func fire(_ employee: any Employee) { }

You’re still allowed to omit it, but this is likely to change in the future. The new keyword creates a distinction between protocol conformance constraints and so-called existential types:

    func fire<Someone: Employee>(_ employee: Someone) { }
func fire(_ employee: any Employee) { }

The difference between the two versions doesn’t seem to be a big deal now–even seasoned Swift programmers might find it subtle. Conceptually, the first method takes an argument of any type that conforms to the Employee protocol, while the second one takes an argument of Employee as a type itself. For the time being, both versions will result in the same behavior, but the keyword is here to stay, so it’s a good idea to start getting used to it.

Type Placeholders

Type annotations for local variables and properties can contain underscore placeholders to take advantage of type inference:

    let int: _ = 1
struct IntContainer {
let int: _ = 1
}

In the above code snippet, it’s not particularly useful since those types are inferred even without placeholders. However, it might be useful for partial inference. For example, the variable in the code extract below would have the type of [String : Int]:

    let dict = ["1" : 1,  "2" : 2]

If we want it to be [String : Double], we can help the compiler with the second part of the type and still leave the first part for the compiler to infer:

    let dict: [_ : Double] = ["1" : 1, "2" : 2] // [String : Double]

Another interesting use case is making an inferred type optional. Just imagine how useful this could be in situations where you have to explicitly specify a very long type only to let the compiler know that it should be optional. Swift 5.6 lets you go with a shorter version:

    let int: _? = SomeVeryLongTypeName() // SomeVeryLongTypeName?

Unfortunately, placeholders don’t work with function parameters and return values. But they do produce useful hints in the latter case and suggest fixes:

    // Type placeholder may not appear in function return type
// Replace the placeholder with the inferred type 'Int' (Fix)
func returnInt() -> _ {
anInt
}

If you try to use a type placeholder with a function parameter, you’ll be shown another funny hint:

alt_text

Figure 1: Attempt to use a function parameter with a type placeholder

Standard Library Additions

CodingKeyRepresentable Protocol

Swift lets you encode dictionaries with String and Int keys into JSON objects of key-value pairs:

    import Foundation

let dict = ["key1" : "value1", "key2" : "value2"]
let data = try! JSONEncoder().encode(dict)

print(String(data: data, encoding: .utf8)!)
// {"key1":"value1","key2":"value2"}

But if you try to use any other type for keys, you’ll end up with an array of alternating keys and values instead of key-value pairs—not very convenient for sending to your backend:

    struct CustomKey: Hashable, Encodable {
let key: String
}

let dict = [CustomKey(key: "key1") : "value1",
CustomKey(key: "key2") : "value2"]
let data = try! JSONEncoder().encode(dict)

print(String(data: data, encoding: .utf8)!)
// [{"key":"key1"},"value1",{"key":"key2"},"value2"]

Now, in Swift 5.6, the CodingKeyRepresentable protocol fixes this issue. What it expects from you is to essentially implement a way to represent your custom key as a String and, optionally, an Int. Of course, this is just a wrapper for existing limitations, but the protocol at least provides a unified way of doing it instead of forcing you to “reinvent the wheel” with your own solution:

    struct CustomCodingKey: CodingKey {


let stringValue: String
let intValue: Int? = nil


init(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
nil
}


}

struct CustomKey: Hashable, Codable, CodingKeyRepresentable {


let id: String
var codingKey: CodingKey { CustomCodingKey(stringValue: id) }


init(id: String) {
self.id = id
}
init?<Key: CodingKey>(codingKey: Key) {
self.init(id: codingKey.stringValue)
}


}

let dict = [CustomKey(id: "key1") : "value1", CustomKey(id: "key2") : "value2"]
let data = try! JSONEncoder().encode(dict)

print(String(data: data, encoding: .utf8)!)

This prints “["key1":"value1","key2":"value2"]”–much better for outside-Swift environments.

Compiler Additions

#unavailable Keyword

The #available keyword now has an inverted counterpart–#unavailable.

First of all, this is useful in situations when you would need to write something extra for older platforms. For example, starting from iOS 13, you might have moved a lot of application initialization stuff to UISceneDelegate and executed a lot of code in UIApplicationDelegate only conditionally, to ensure backward compatibility with previous iOS versions:

    if #available(iOS 13.0, *) {
// Do nothing. The case is covered in UISceneDelegate.
} else {
// ... (execute pre-iOS 13 code).
}

Swift 5.6 allows you to make such code look a little bit nicer:

    if #unavailable(iOS 13.0) {
// ... (execute pre-iOS 13 code).
}

In the #unavailable case, you of course don’t need a wildcard as the second argument–another small advantage to making your code a bit more concise.

Conclusion

Although Swift 5.6 doesn’t seem to bring us a pile of new exciting possibilities like Swift 5.5 did, it’s another important step towards Swift 6. After introducing a built-in concurrency with the previous language release, it’s not surprising that the authors continue building a path for us to slowly start using this feature in real projects.

Another important thing about this new release is the existential “any” keyword. If I had to bet, this keyword is going to be mandatory in one of the upcoming major releases. Whether or not this actually happens, Swift users should keep up with what’s going on in the field.