Blog Article

What’s New In Swift 5.0


Aasif Khan
By Aasif Khan | Last Updated on December 14th, 2023 12:19 pm | 5-min read

A new version of the Swift programming language is here: Swift 5.0. It’s released in March 2019, with a fair number of changes. How does this Swift update affect practical iOS development? And who’s making these changes anyway? In this tutorial, we’ll walk through some of the proposed and accepted changes for Swift 5.0. We’ll also discuss how the process of making changes to the Swift language works, and why that’s relevant for iOS developers. The goal of this tutorial isn’t to provide you with an exhaustive list of upcoming Swift 5.0 changes (that’s impossible), but to instead give you some insight into how the Swift language is being developed. It’s also an opportunity for Swift developers to learn more about Swift, its contributors, and what goes into developing a programming language.

How The Swift 5.0 Update Works

The Swift programming language first appeared in 2014. Swift is stable to use in production apps, but the language is also actively being developed. About five years later, we’re already at version 4.2! At the end of 2015, the Swift language has been open sourced. That means that anyone can read, copy and modify the source code that makes up the Swift language. As a result, the open source community now actively improves the language – with 600+ contributors. Swift was originally the “second” language for apps in the Apple ecosystem (iOS, macOS, tvOS, watchOS) next to Objective-C. But since it’s open source, Swift can be used on a number of other platforms, most notably Linux and Ubuntu. This lead to the development of server-side Swift, i.e. using Swift as a back-end development language for webservices. Why discuss all this? It’s important to realize that Swift is much bigger than building iOS apps – and as you’ll soon see, this is even more true for Swift 5.0. In short, updates to the Swift programming language go according to this process:
  • Changes to the Swift programming language are proposed and discussed on a GitHub repository called Swift Evolution.
  • Anyone can propose changes to the language, but there’s process that needs to be followed. You’ll find that many proposed changes are first discussed in the Swift Forums, and in communities beyond.
  • Proposed changes are publicly reviewed by the Swift core team, consisting of (former) Apple engineers like Chris Lattner and Ted Kremenek. A list of current and past proposals can be found on Swift Evolution, in the proposals directory.
  • Proposals typically include source code or technical designs. Eventually, if a proposal is accepted, the source code for the change is merged into the Swift source code. (Yes, programming languages are programmed, too!)
  • And of course, proposals can be rejected too. A rejected proposal that’s worth checking out is SE-0217, about the bangbang operator !!. This proposal is fairly easy to understand, and has lead to good discussions and insights in the Swift developer community. It’s a good example of how Swift Evolution is about more than just creating a good programming language.
A good primer into Swift Evolution is reading through the Commonly Rejected Changes document. It outlines changes to the Swift languages that have been brought up often, were rejected by the core Swift team, and why. They’re easy to comprehend, even for beginner developers, so it’s a great idea to work your way through them if you’re new to the Swift language and the workings of Swift Evolution. How do you navigate the Swift Evolution repository? It’s easiest to check out the proposals directory. Scroll down to see the most recent proposals. Some of the commit messages mention Swift 5, so it’s simple to filter those out. You can also clone the repository on your local computer and text-search the proposals for tags like “Status: Implemented (Swift 5)”. Now that you know a bit more about how Swift is developed, what’s up with Swift 5.0?

The Goal For Swift 5.0: 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. Now that that’s out of the way, let’s check out some actual Swift 5.0 changes!

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. SE-0220 describes a new function on collections that combines filter(_:) and count in one function call. 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. let scores = [1, 3, 8, 2, 5, 6, 2, 10] let result = scores.filter({ $0 > 5 }) print(result) // Output: [8, 6, 10] 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: let scores = [1, 3, 8, 2, 5, 6, 2, 10] let count = scores.filter({ $0 > 5 }).count print(count) // Output: 3 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: let scores = [1, 3, 8, 2, 5, 6, 2, 10] let count = scores.count(where: { $0 > 5 }) print(count) // Output: 3 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. Neat! Looking for a challenge? See if you can find count(where:) in the Swift standard library and read its source code. You can even compare its implementation to functions like filter(_:).

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. let result = 5 % 2 == 0 print(result) // Output: false SE-0225 proposes and implements a new function isMultiple(of:) for integers that checks if a given number is a multiple of another number. Here’s how you use it: let number = 42 if number.isMultiple(of: 2) { print("\(number) is even!") } 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 Type

SE-0235 is pretty cool, because it adds an entirely new type to Swift: Result. 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 tutorial 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: public enum Result { case success(Success), failure(Failure) } 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: dataTask(with: url) { (result: Result) in switch result { case let .success(data): handleResponse(data: data) case let .error(error): handleError(error) } } 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

SE-0192 proposes a solution to the following conundrum:
  • 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 exhausive. 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: enum Fruit { case apple case orange case banana } We write some code to handle this enumeration, to purchase different fruits for example. Like this: let fruit = ... switch fruit { case .apple: print("Purchasing apple for $0.50") case .orange: print("Purchasing orange for $0.39") case default: print("We don't sell that kind of fruit here.") } 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 can consider 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. switch fruit { case .apple: ... case @unknown default: print("We don't sell that kind of fruit here.") } 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>. 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. So, you say – why not just make try? work the same way as as?. Good point! That’s exactly what SE-0230 does. It flattens nested optionals resulting from try?, giving it the same behavior as as? and optional chaining. Neat!

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
SE-0218 changes that, and adds the compactMapValues(_:) function to dictionaries. This function combines the compactMap(_:) function of arrays with the mapValues(_:) function of dictionaries, effectively mapping-and-filtering dictionary values. 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: let ages = [ "Mary": "42", "Bob": "twenty-five har har har!!", "Alice": "39", "John": "22" ] let filteredAges = ages.compactMapValues({ Int($0) }) print(filteredAges) // Output: ["Mary": 42, "Alice": 39, "John": 22] 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). Neat!

Further Reading

Pfew! And that’s not even everything that’s changing in Swift 5.0… In this tutorial, you’ve learned why and how Swift changes, and we’ve looked at a few interesting examples of upcoming changes for Swift 5.0. Fortunately it’s mostly good news, and Swift doesn’t change that much, but some of the changes are definitely code-breaking. The main goal for Swift 5.0 is to reach ABI stability, and we’ve discussed what that means. It’ll become easier to package Swift with an operating system, and it’s a big milestone for the maturity of the Swift language.

Related Articles

Aasif Khan

Head of SEO at Appy Pie