5 small but significant improvements in Swift 5.1
Discover page available: The Standard LibrarySwift 5.1 has now been officially released, and despite being a minor release, it contains a substantial number of changes and improvements — ranging from fundamental new features, like module stability (which enables SDK vendors to ship pre-compiled Swift frameworks), to all of the new syntax features that power SwiftUI, and beyond.
Besides its headlining new features, Swift 5.1 also contains a number of smaller — but still highly significant — new capabilities and improvements. It’s the sort of changes that at first may seem really minor, or even unnecessary, but can turn out to have a quite major impact on how we write and structure our Swift code. This week, let’s take a look at five of those features, and what kind of situations they could be useful in.
Memberwise initializers with default values
One of the many things that make structs so appealing in Swift is their auto-generated “memberwise” initializers — which enable us to initialize any struct (that doesn’t contain private stored properties) simply by passing values corresponding to each of its properties, like this:
struct Message {
var subject: String
var body: String
}
let message = Message(subject: "Hello", body: "From Swift")
These synthesized initializers have been significantly improved in Swift 5.1, since they now take default property values into account, and automatically translate those values into default initializer arguments.
Let’s say that we wanted to expand the above Message
struct with support for attachments, but that we’d like the default value to be an empty array — and at the same time, we’d also like to enable a Message
to be initialized without having to specify a body
up-front, so we’ll give that property a default value as well:
struct Message {
var subject: String
var body = ""
var attachments: [Attachment] = []
}
In Swift 5.0 and earlier, we’d still have to pass initializer arguments for all of the above properties anyway, regardless of whether they have a default value. However, in Swift 5.1, that’s no longer the case — meaning that we can now initialize a Message
by only passing a subject
, like this:
var message = Message(subject: "Hello, world!")
That’s really cool, and it makes using structs even more convenient than before. But perhaps even cooler is that, just like when using standard default arguments, we can still override any default property value by passing an argument for it — which gives us a ton of flexibility:
var message = Message(
subject: "Hello, world!",
body: "Swift 5.1 is such a great update!"
)
However, while memberwise initializers are incredibly useful within an app or module, they’re still not exposed as part of a module’s public API — meaning that if we’re building some form of library or framework, we still have to define our public-facing initializers manually (for now).
Using Self
to refer to enclosing types
Swift’s Self
keyword (or type, really) has previously enabled us to dynamically refer to a type in contexts where the actual concrete type isn’t known — for example by referring to a protocol’s implementing type within a protocol extension:
extension Numeric {
func incremented(by value: Self = 1) -> Self {
return self + value
}
}
While that’s still possible, the scope of Self
has now been extended to also include concrete types — like enums, structs and classes — enabling us to use Self
as a sort of alias referring to a method or property’s enclosing type, like this:
extension TextTransform {
static var capitalize: Self {
return TextTransform { $0.capitalized }
}
static var removeLetters: Self {
return TextTransform { $0.filter { !$0.isLetter } }
}
}
The fact that we can now use Self
above, rather than the full TextTransform
type name, is of course purely syntactic sugar — but it can help make our code a bit more compact, especially when dealing with long type names. We can even use Self
inline within a method or property as well, further making the above code even more compact:
extension TextTransform {
static var capitalize: Self {
return Self { $0.capitalized }
}
static var removeLetters: Self {
return Self { $0.filter { !$0.isLetter } }
}
}
Besides referring to an enclosing type itself, we can now also use Self
to access static members within an instance method or property — which is quite useful in situations when we want to reuse the same value across all instances of a type, such as the cellReuseIdentifier
in this example:
class ListViewController: UITableViewController {
static let cellReuseIdentifier = "list-cell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(
ListTableViewCell.self,
forCellReuseIdentifier: Self.cellReuseIdentifier
)
}
}
Again, we could’ve simply typed out ListViewController
above when accessing our static property, but using Self
does arguably improve the readability of our code — and will also enable us to rename our view controller without having to update the way we access its static members.
Switching on optionals
Next, let’s take a look at how Swift 5.1 makes it easier to perform pattern matching on optionals, which really comes in handy when switching on an optional value. As an example, let’s say that we’re working on a music app that contains a Song
model — which has a downloadState
property that lets us keep track of whether a song has been downloaded, if it’s currently being downloaded, and so on:
struct Song {
...
var downloadState: DownloadState?
}
The reason the above property is an optional is that we want nil
to represent the lack of a download state, that is, if a song hasn’t been downloaded at all.
Like we took a look at in “Pattern matching in Swift”, Swift’s advanced pattern matching capabilities enable us to directly switch on an optional value — without having to unwrap it first — however, before Swift 5.1, doing so required us to append a question mark to each matching case, like this:
func songDownloadStateDidChange(_ song: Song) {
switch song.downloadState {
case .downloadInProgress?:
showProgressIndiator(for: song)
case .downloadFailed(let error)?:
showDownloadError(error, for: song)
case .downloaded?:
downloadDidFinish(for: song)
case nil:
break
}
}
In Swift 5.1, those trailing question marks are no longer needed, and we can now simply refer to each case directly — just like when switching on a non-optional value:
func songDownloadStateDidChange(_ song: Song) {
switch song.downloadState {
case .downloadInProgress:
showProgressIndiator(for: song)
case .downloadFailed(let error):
showDownloadError(error, for: song)
case .downloaded:
downloadDidFinish(for: song)
case nil:
break
}
}
While the above is a really welcome change in terms of further reducing the syntax required to implement common patterns, it does come with a slight side-effect, that could potentially be source-breaking for certain enums and switch statements.
Since Swift optionals are implemented using the Optional
enum under the hood, we’re no longer able to perform the above kind of optional pattern matching on any enum that contains either a some
or none
case — since those will now conflict with the cases that Optional
contains.
However, it can be argued that any enum that contains such cases (especially none
), should instead be implemented using an optional — since representing potentially missing values is essentially what optionals do.
The Identifiable
protocol
Originally introduced as part of the initial release of SwiftUI, the new Identifiable
protocol has now made its way into the Swift standard library — and provides a simple and unified way to mark any type as having a stable, unique identifier.
To conform to this new protocol, we simply have to declare an id
property, which can contain any Hashable
type — for example String
:
struct User: Identifiable {
typealias ID = String
var id: ID
var name: String
}
Similar to when Result
was added to the standard library as part of Swift 5.0, a major benefit of now having Identifable
accessible to any Swift module is that it can be used to share requirements across different code bases.
For example, using a constrained protocol extension, we could add a convenience API for transforming any Sequence
that contains identifiable elements into a dictionary — and then vend that extension as part of a library, without requiring us to define any protocol of our own:
public extension Sequence where Element: Identifiable {
func keyedByID() -> [Element.ID : Element] {
var dictionary = [Element.ID : Element]()
forEach { dictionary[$0.id] = $0 }
return dictionary
}
}
The above API is implemented as a method, rather than as a computed property, since its time complexity is O(n)
. For more on picking between a method and a computed property, see this article.
However, while the standard library’s new Identifiable
protocol is really useful when dealing with collections of values that each have a stable identifier, it doesn’t do much to improve the actual type safety of our code.
Since all that Identifiable
does is requiring us to define any hashable id
property, it won’t protect us from accidentally mixing up identifiers — such as in this situation, when we’re mistakenly passing a User
ID to a function that accepts a Video
ID:
postComment(comment, onVideoWithID: user.id)
So there’s still quite a lot of strong use cases for a proper Identifier
type and a more robust Identifiable
protocol — such as the ones we took a look at in “Type-safe identifiers in Swift”, which prevents the above kind of mistakes from happening. However, it’s still really nice to now have some version of an Identifiable
protocol in the standard library, even if it is a bit more limited.
Ordered collection diffing
Finally, let’s take a look at a brand new standard library API that’s being introduced as part of Swift 5.1 — ordered collection diffing. As we, as a community, move closer and closer to the world of declarative programming with tools like Combine and SwiftUI — being able to calculate the difference between two states is something that becomes increasingly important.
After all, declarative UI development is all about continuously rendering new snapshots of state — and while SwiftUI and the new diffable data sources will probably do most of the heavy lifting to make that happen — being able to calculate a diff between two states ourselves could be incredibly useful.
For example, let’s say that we’re building a DatabaseController
that’ll let us easily update our on-disk database with an array of in-memory models. To be able to figure out whether a model should be inserted or deleted, we can now simply call the new difference
API to calculate the diff between our old array and the new one — and then iterate through the changes within that diff in order to perform our database operations:
class DatabaseController<Model: Hashable & Identifiable> {
private let database: Database
private(set) var models: [Model] = []
...
func update(with newModels: [Model]) {
let diff = newModels.difference(from: models)
for change in diff {
switch change {
case .insert(_, let model, _):
database.insert(model)
case .remove(_, let model, _):
database.delete(model)
}
}
models = newModels
}
}
However, the above implementation does not account for moved models — since moves will, by default, be treated as separate insertions and removals. To fix that, let’s also call the inferringMoves
method when computing our diff — and then look at whether each insert was associated with a removal, and if so treat it as a move instead, like this:
func update(with newModels: [Model]) {
let diff = newModels.difference(from: models).inferringMoves()
for change in diff {
switch change {
case .insert(let index, let model, let association):
// If the associated index isn't nil, that means
// that the insert is associated with a removal,
// and we can treat it as a move.
if association != nil {
database.move(model, toIndex: index)
} else {
database.insert(model)
}
case .remove(_, let model, let association):
// We'll only process removals if the associated
// index is nil, since otherwise we will already
// have handled that operation as a move above.
if association == nil {
database.delete(model)
}
}
}
models = newModels
}
The fact that diffing is now built into the standard library (and also into UIKit and AppKit) is fantastic news — as writing an efficient, flexible, and robust diffing algorithm can be incredibly difficult.
Conclusion
Not only is Swift 5.1 a key enabler for SwiftUI and Combine, it’s also big news for any team that vends pre-compiled frameworks, as Swift is now not only ABI stable, but also module stable. On top of that, Swift 5.1 also includes many small but welcome changes and tweaks that should be applicable to almost any code base — and while we’ve taken a look at five of those changes in this article, we’ll keep diving deeper into more aspects of Swift 5.1 within the coming weeks and months.
The reason no SwiftUI-related features were included in this article is that those were covered in “The Swift 5.1 features that power SwiftUI’s API”. The same is also true for static subscripts, which were covered in “The power of subscripts in Swift”.
What do you think? Have you already migrated your projects to Swift 5.1, and if so, what’s your favorite new feature? Let me know — along with any questions, comments or feedback that you might have — either via Twitter or email.
Thanks for reading! 🚀