What’s new in Swift 5

MANVENDRA SINGH RATHOR
9 min readMay 22, 2021

ABI Stability

The main focus for Swift 5.0 is to reach ABI stability. The term “ABI” stands for Application Binary Interface. It’s the binary equivalent of an API, an Application Programming Interface.

As an iOS developer, you use the API of a library to write code for your apps. The UIKit framework, for example, includes an API to interface with buttons, labels and view controllers.

You use that API as a developer, just like you use the steering wheel to drive a car. Some APIs are private, such as your car’s odometer. Roughly speaking, the steering wheel connects internally to the wheels of your car. When you steer the car, via the steering wheel “API”, the car turns.

The way the steering wheel connects to the car’s wheels is the Application Binary Interface (ABI). Internally, the steering wheel is connected to a shaft between the car’s wheels that turns the wheels. You have no control over how this system works — the only thing you can do is steering.

When a user downloads and installs your app, they’re not downloading all the code that your app needs to execute properly. Much of that code is already present on their iPhone, as part of iOS, and its frameworks and libraries. Your app can use the binary code already present, via the ABI.

But what if your app uses a different version of a certain framework? Different versions mean different APIs and ABIs. Oops! Fortunately, most iOS versions are backward (and forward) compatible, at least for a few iOS versions. Apps compiled with the iOS 11 SDK run on iOS 10, and vice versa (some exceptions apply here).

Let’s get back to Swift now.

The problem with Swift is that its Application Binary Interface (ABI) isn’t stable. It’s not stable enough to ship as part of iOS, like other frameworks and libraries.

ABI stability in this sense means that any future compilers of Swift need to be able to produce binaries that can run on the stable ABI that’s part of iOS. The Swift core team isn’t confident yet that the ABI is stable enough to be shipped as part of iOS, but that’s going to change with Swift 5.0.

The solution so far has been to ship iOS apps that use Swift with the Swift dynamic binary itself. Instead of connecting to the OS, every app links to their own specific version of Swift!

The main advantage is that app developers can develop apps with Swift, while Swift is actively being developed, without running the risk of ABI incompatibility. Changes to Swift can be distributed without updating iOS.

This has negative consequences, too:

When an ABI is stable, operating system vendors can safely include the Swift binary interface with releases of their operating system. This severely impacts the adoption of Swift outside of the Apple ecosystem.

Every Swift app ships with a version of the Swift dynamic libraries. This takes up storage space and bandwidth — it’s a waste of resources.

A stable ABI means that the language cannot change in major ways, and less frequent, because the Swift library is shipped with iOS. For those who remember the Swift 2 to 3 migration, this is a good thing…

Developers will be able to distribute pre-compiled frameworks, i.e. binaries, of their libraries. This means you don’t have to compile a library before using it, which affects Xcode compile times, but also paves the way for Swift-based commercial libraries and binary packages for Linux.

In short, ABI stability is an important milestone for Swift “growing up” and becoming more mainstream outside of iOS. It’s also a requirement for companies and products transitioning to Swift-only apps and libraries.

Filter And Count With “count(where:)”

The proposed count(where:) function has been retracted for the Swift 5.0 release, because of performance issues, but it’s hopefully coming back in a future version of Swift.

You’re already familiar with collection functions like map(_:), reduce(_:) and filter(_:). You use filter(_:) to find and return array items that conform to a given predicate, passed as a closure.

Let’s look at an example. In the code below, we’re using filter(_:) to filter out numbers from scores that are greater than 5.

In practical iOS development, you often want to find/filter values from an array — and then just count them. Here’s the same example, using the count property to find the number of filtered items:

The above code has two problems:

It’s a bit too verbose. First we’re filtering, and then we’re counting, even though we really only want to count. And when reading, it’s easy to miss the .count at the end, too.

The above code is wasteful. The intermediate result of the filter(_:) operation is discarded. It’s like copying the apples from a fruit basket, only to throw them away once you’re done counting.

You might as well filter and count in one operation, right? That’s what the count(where:) function does. It’s newly added to Swift 5.0!

Here’s how:

count method in swift 5

Just like before, we’re filtering and counting the scores array. How many items are greater than 5? This time, however, the filtering and counting is done in one go, with one function call.

Integer Multiples With “isMultiple(of:)”

A common use case in practical programming is testing if one number is divisible by another number. Many, if not most, of these tests involve checking if a number is even or odd.

The default approach is to use the remainder operator %. The code below checks if the remainder of 5 divided by 2 is equal to 0. It’s not, because the remainder is 1. As such, 5 is an odd number.

Simple, right? Reasons for including it in the Swift standard library were:

It improves readability of your code, because the sentence reads like common English: if number is multiple of 2.

Functions are discoverable by using code completion in Xcode, so that may help developers who are unaware of the % operator, or can’t find it.

It’s not uncommon to make mistakes with the % operator, and its implementation differs across languages a developer may use.

It’s worth noting here that this addition to the Swift language shows why changes to the language are made. It’s not just about “better”, but “better for whom?” Who benefits when the community adds this function to the language?

The new isMultiple(of:) function isn’t more performant than %, and it isn’t more clever. What does it mean to be “better”? In this case, it’s readability, discoverability, safety and convenience for developers.

The Result<Success, Failure: Error> Type

You already know how to handle errors in Swift with do-try-catch, right? You also probably know that many functions still pass Error or NSError values, from async APIs for example.

In this article about URLSession you can see first-hand how complex it is to handle different kinds of errors elegantly. You basically need 2–3 different mechanisms to deal with try, Error and guard value != nil ….

The community responded to this issue by introducing a Result type in their projects. This type encompasses two states of a passed result: success or failure. And because this type is so widely used, it’s now being added to the Swift standard library.

This is the proposed solution:

The above code defines an enumeration with two cases: .success and .failure. These cases have associated types Success and Failure. Every type here is generic, so Success can be any value, but the value you pass for Failure needs to conform to the Error protocol.

In short, this Result type can now be used as the argument passed in a completion handler for an async function call. Like this:

The Result type encapsulates possible return values and errors in one object. It also uses the power of enumerations to help you write expressive code, i.e. a switch block that gracefully deals with return values and errors.

This change to Swift is definitely more involved than just adding a new convenient function, so it’s worth it to investigate it further. You could even call it philosophical: the Swift language designers actively discuss how the language is used, as opposed to just providing Swift’s syntax.

Handling Future Enum Cases

When you switch over an enum, you’ll need to do so exhaustively (i.e., add a switch case for every case in the enumeration)

You may need to add a case to an enum in the future, which is a code-breaking change for any switch that uses that enum

Switching over enums needs to be exhaustive, so when you add a new case to an enum that’s switched over, code you wrote earlier will break because the switch is no longer exhaustive. It needs to consider the new enum case.

This is particularly cumbersome for code from libraries, SDKs and frameworks, because every time you’re adding a new case to an enum you’re breaking someone else’s code. Moreover, a code breaking change negatively affects that binary compatibility we talked about.

Take this enum, for example:

We write some code to handle this enumeration, to purchase different fruits for example. Like this:

The above switch block is exhaustive, because we’ve added default. However, if you added all three .apple, .orange and .banana cases, and you or another developer adds a new case to Fruit, your code would crash.

The solution is two-fold:

Enumerations in the Swift standard library, and imported from elsewhere, can be either frozen or non-frozen. A frozen enumeration cannot change in the future. A non-frozen enum can change in the future, so you’ll need to deal with that.

When switching over a non-frozen enum, i.e. one that can change in the future, you should include a “catch-all” default case that matches any values that the switch doesn’t already explicitly match. If you don’t use default when you need to, you’ll get a warning.

This introduces another problem: how do you know if your default matches a value that you explicitly don’t want to match (i.e., catch-all) or that it’s a new enum value that has been added later on? The Swift compiler doesn’t know that either — so it can’t warn you that you have non-matched cases in a switch block!

Consider that a new fruit case is added to Fruit, such as .pineapple. How do we know that we could sell that piece of fruit but won’t, or that it’s newly added by the framework and that we canconsider selling it? Swift has no way to distinguish between these cases, and can’t warn us about it.

In Swift 5.0, a new @unknown keyword can be added to the default switch case. This doesn’t change the behavior of default, so this case will still match any cases that aren’t handled in the rest of the switch block.

The @unknown keyword will trigger a warning in Xcode if you’re dealing with a potentially non-exhaustive switch statement, because of a changed enumeration. You can deliberately consider this new case, thanks to the warning, which wasn’t possible with just default.

And the good thing is that due to how default works, your code won’t break if new cases are added to the enum — but you do get warned. Neat!

Flatten Nested Optionals With “try?”

Nested optionals are… strange. Here’s an example:

let number:Int?? = 5

print(number)

// Output: 5

The above number constant is doubly wrapped in an optional, and its type is Int?? or Optional<Optional<Int>>. Although it’s OK to have nested optionals, it’s also downright confusing and unnecessary.

Swift has a few ways to avoid accidentally ending up with nested optionals, for example in casting with as? and in optional chaining. However, when using try? to convert errors to optionals, you can still end up with nested optionals.

Here’s an example:

let car:Car? = …

let engine = try? car?.getEngine()

In the above example, car is an optional of type Car?. On the second line, we’re using optional chaining, because car is an optional. The return value of the expression car?.getEngine() is optional too, because of the optional chaining. It’s type is Engine?.

When you combine that with try?, whose return value is also an optional, you get a double or nested optional. As a result, the type of engine is Engine??. And that causes problems, because to get to the value (or not) you’ll have to unwrap twice.

Because as? already flattens optionals, one way to get out of the nested optional is this mind-boggling bit of code:

if let engine = (try? car?.getEngine()) as? Engine {

// OMG!

}

The code optional casts Engine?? to Engine, which will flatten the optionals because of the way as? works. The cast itself is useless, because we’re working with that Engine type anyway.

The New “compactMapValues()” Function For Dictionaries

The Swift standard library includes two useful functions for arrays and dictionaries:

The map(_:) function applies a function to array items, and returns the resulting array, while the compactMap(_:) function also discards array items that are nil

The mapValues(_:) function does the same for dictionaries, i.e. it applies a function to dictionary values, and returns the resulting dictionary — but it doesn’t have a nil discarding counterpart.

Consider a scenario where you surveyed your family members for their ages, in integer numbers, but your stupid uncle Bob managed to spell out his age instead…

So, you run the following code:

The compactMapValues(_:) function is helpful in scenarios where you want to remove nil values from a dictionary, or when you’re using failable initializers to transform dictionary values (see above).

Thanks for reading.

Hope this article may be helpful to you.

Soon, I will come up with Swift 5.3 update.

If you like the post then Follow me : https://www.linkedin.com/in/manvendra-singh-rathore-779003a8

--

--