New Features in Swift 5.4

Swift 5.4 brings with it some huge compilation improvements, including better code completion in expressions with errors and big speed ups for incremental compilation. However, it also adds some important new features and refinements, so let’s dig into them here…

Improved implicit member syntax

Improves Swift’s ability to use implicit member expressions, so rather than just having support for exactly one single static member you can make chains of them.

Swift has always had the ability to use implicit member syntax for simple expressions, for example if you wanted to color some text in SwiftUI you could use .red rather than Color.red:

struct ContentView: View {
var body: some View {
Text("Hey, welcome in swift world!")
.foregroundColor(.red)
}
}

Prior to Swift 5.4 this did not work with more complex expressions. For example, if you wanted your red color to be slightly transparent you would need to write this:

struct ContentView1: View {
var body: some View {
Text("Hey, welcome in swift world!")
.foregroundColor(Color.red.opacity(0.5))
}
}

From Swift 5.4 onwards the compiler is able to understand multiple chained members, meaning that the Color type can be inferred:

struct ContentView2: View {
var body: some View {
Text("Hey, welcome in swift world!!")
.foregroundColor(.red.opacity(0.5))
}
}

Multiple variadic parameters in functions

Introduced the ability to have functions, subscripts, and initializers use multiple variadic parameters as long as all parameters that follow a variadic parameter have labels. Before Swift 5.4, you could only have one variadic parameter in this situation.

So, with this improvement in place we could write a function that accepts a variadic parameter storing the times goals were scored during a cricket match, plus a second variadic parameter scoring the names of players who scored:

func summarizeGoals(times: Int..., players: String...) {
let joinedNames = ListFormatter.localizedString(byJoining: players)
let joinedTimes = ListFormatter.localizedString(byJoining: times.map(String.init))

To call that function, provide both sets of values as variadic parameters, making sure that all parameters after the first variadic are labeled:

summarizeGoals(times: 10, 63, 95, 60, players: "Michel", "Kia", "Roy")

Result builders

Function builders unofficially arrived in Swift 5.1, but in the run up to Swift 5.4 they formally went through the Swift Evolution proposal process as in order to be discussed and refined. As part of that process they were renamed to result builders to better reflect their actual purpose, and even acquired some new functionality.

First up, the most important part: result builders allow us to create a new value step by step by passing in a sequence of our choosing. They power large parts of SwiftUI’s view creation system, so that when we have a VStack with a variety of views inside, Swift silently groups them together into an internal TupleView type so that they can be stored as a single child of the VStack – it turns a sequence of views into a single view.

Result builders deserve their own detailed article, but I at least want to give you some small code examples so you can see them in action.

Here is a function that returns a single string:

func makeSentence() -> String {
"Why settle for a Joe when you can have a Prince?"
}

That works great, but what if had several strings we wanted to join together? Just like SwiftUI, we might want to provide them all individually and have Swift figure it out:

// This is invalid Swift, and will not compile.
// func makeSentence1() -> String {
// "Why settle for a Joe"
// "when you can have"
// "a Prince?"
// }

By itself, that code won’t work because Swift no longer understands what we mean. However, we could create a result builder that understands how to convert several strings into one string using whatever transformation we want, like this:

@resultBuilder
struct SimpleStringBuilder {
static func joinBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}
}

Even though that’s a small amount of code, there’s a lot to unpack:

  • The @resultBuilder attribute tells SwiftUI the following type should be treated as a result builder. Previously this behavior was achieved using @_functionBuilder, which had an underscore to show that this wasn’t designed for general use.
  • Every result builder must provide at least one static method called buildBlock(), which should take in some sort of data and transform it. The example above takes in zero or more strings, joins them, and sends them back as a single string.
  • The end result is that our SimpleStringBuilder struct becomes a result builder, meaning that we can use @SimpleStringBuilder anywhere we need its string joining powers.

There’s nothing to stop us from using SimpleStringBuilder.buildBlock() directly, like this:

let joined = SimpleStringBuilder.buildBlock(
"Why settle for a Joe",
"when you can have",
"a Prince?"
)

However, because we used the @resultBuilder annotation with our SimpleStringBuilder struct, we can also apply that to functions, like this:

@SimpleStringBuilder func makeSentence2() -> String {
"Why settle for a Joe"
"when you can have"
"a Prince?"
}

Notice how we no longer need the commas at the end of each string — @resultBuilder automatically transforms each statement in makeSentence() into a single string by using SimpleStringBuilder.

In practice, result builders are capable of significantly more, accomplished by adding more methods to your builder type. For example, we could add if/else support to our SimpleStringBuilder by adding two extra methods that describe how we want to transform the data. In our code we don’t want to transform our strings at all, so we can send them right back:

@resultBuilder
struct ConditionalStringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}

I know that looks like we’ve done almost no work, but now our functions are able to use conditions:

@ConditionalStringBuilder func makeSentence3() -> String {
"Why settle for a Joe"
"when you can have"

Similarly, we could add support for loops by adding a buildArray() method to our builder type:

@resultBuilder
struct ComplexStringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}

And now we can use for loops:

@ComplexStringBuilder func countDown() -> String {
for i in (0...10).reversed() {
"\(i)…"
}

It feels almost like magic because the result builder system is doing almost all the work for us, and even though our example has been fairly simple I hope you can get a taste for the remarkable power result builders bring to Swift.

It’s worth adding that Swift 5.4 extends the result builder system to support attributes being placed on stored properties, which automatically adjusts the implicit memberwise initializer for structs to apply the result builder.

This is particularly helpful for custom SwiftUI views that use result builders, such as this one:

struct CustomVStack<Content: View>: View {
@ViewBuilder let content: Content

If you’d like to see more advanced, real-world examples of result builders in action, you should check out the Awesome Function Builders repository on GitHub.

Local functions now support overloading

Requested the ability to overload functions in local contexts, which in practice means nested functions can now be overloaded so that Swift chooses which one to run based on the types that are used.

For example, if we wanted to make some simple cookies we might write code like this:

struct Butter { }
struct Milk { }
struct Sugar { }

Prior to Swift 5.4, the three add() methods could be overloaded only if they were not nested inside makeCookies(), but from Swift 5.4 onwards function overloading is supported in this case as well.

Property wrappers are now supported for local variables

Property wrappers were first introduced in Swift 5.1 as a way of attaching extra functionality to properties in an easy, reusable way, but in Swift 5.4 their behavior got extended to support using them as local variables in functions.

For example, we could create a property wrapper that ensures its value never goes below zero:

@propertyWrapper struct GetNonNegative<T: Numeric & Comparable> {
var value: T

And from Swift 5.4 onwards we can use that property wrapper inside a regular function, rather than just attaching to a property. For example, we might write a game where our player can gain or lose points, but their score should never go below 0:

func playGame() {
@GetNonNegative var score = 0

Packages can now declare executable targets

Add a new target option for apps using Swift Package manager, allowing us to explicitly declare an executable target.

This is particularly important for folks who want to use SE-0281 (using @main to mark your program’s entry point), because it didn’t play nicely with Swift Package Manager – it would always look for a main.swift file.

With this change, we can now remove main.swift and use @main instead. Note: You must specify // swift-tools-version:5.4 in your Package.swift file in order to get this new functionality.

I am an iOS developer having an experience in objective c and swift.