Multiple variadic parameters, improved implicit member syntax, result builders, and more!
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…
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
SE-0287 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 ContentView1: View {
var body: some View {
Text("You're not my supervisor!")
.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 ContentView2: View {
var body: some View {
Text("You're not my supervisor!")
.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 ContentView3: View {
var body: some View {
Text("You're not my supervisor!")
.foregroundColor(.red.opacity(0.5))
}
}
SE-0284 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 football 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))
print("\(times.count) goals where scored by \(joinedNames) at the follow minutes: \(joinedTimes)")
}
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: 18, 33, 55, 90, players: "Dani", "Jamie", "Roy")
From Swift 5.4 onwards it’s possible to create a local variable by calling a function of the same name. That might sound obscure, but it’s actually a problem we hit all the time.
For example, this creates a struct with a color(forRow:)
method, which gets called and assigned to a local variable called color
:
struct Table {
let count = 10
func color(forRow row: Int) -> String {
if row.isMultiple(of: 2) {
return "red"
} else {
return "black"
}
}
func printRows() {
for i in 0..<count {
let color = color(forRow: i)
print("Row \(i): \(color)")
}
}
}
let table = Table()
table.printRows()
That kind of usage is only allowed from Swift 5.4 and later. In earlier versions of Swift, it would create a circular reference because Swift couldn’t distinguish between the local color
constant and the color(forRow:)
method it was calling – you would have seen the error “Variable used within its own initial value”.
This usually resulted in us either using self.color(forRow: 1989)
to make it clear we mean the method call, or just naming the local value something else such as colorForRow
.
Fortunately Swift 5.4 resolves this and allows us to use the more natural naming.
This change also allows us to make local copies of properties and global variables. For example, we can take a copy of a username
property that is also called username
, like this:
struct User {
let username = "Taylor"
func suggestAlternativeUsername() -> String {
var username = username
username += String(Int.random(in: 1000...9999))
return username
}
}
let user = User()
user.suggestAlternativeUsername()
Because this also applies to global variables, that same code works just fine even without the struct in place:
let username = "Taytay"
func suggestAlternativeUsername() -> String {
var username = username
username += String(Int.random(in: 1000...9999))
return username
}
suggestAlternativeUsername()
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 SE-0289 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 makeSentence1() -> String {
"Why settle for a Duke when you can have a Prince?"
}
print(makeSentence1())
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 makeSentence2() -> String {
// "Why settle for a Duke"
// "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 buildBlock(_ parts: String...) -> String {
parts.joined(separator: "\n")
}
}
Even though that’s a small amount of code, there’s a lot to unpack:
@resultBuilder
attribute tells Swift 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.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.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 Duke",
"when you can have",
"a Prince?"
)
print(joined)
However, because we used the @resultBuilder
annotation with our SimpleStringBuilder
struct, we can also apply that to functions, like this:
@SimpleStringBuilder func makeSentence3() -> String {
"Why settle for a Duke"
"when you can have"
"a Prince?"
}
print(makeSentence3())
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")
}
static func buildEither(first component: String) -> String {
return component
}
static func buildEither(second component: String) -> String {
return component
}
}
I know that looks like we’ve done almost no work, but now our functions are able to use conditions:
@ConditionalStringBuilder func makeSentence4() -> String {
"Why settle for a Duke"
"when you can have"
if Bool.random() {
"a Prince?"
} else {
"a King?"
}
}
print(makeSentence4())
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")
}
static func buildEither(first component: String) -> String {
return component
}
static func buildEither(second component: String) -> String {
return component
}
static func buildArray(_ components: [String]) -> String {
components.joined(separator: "\n")
}
}
And now we can use for
loops:
@ComplexStringBuilder func countDown() -> String {
for i in (0...10).reversed() {
"\(i)…"
}
"Lift off!"
}
print(countDown())
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
var body: some View {
VStack {
// custom functionality here
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.
SR-10069 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 Flour { }
struct Sugar { }
func makeCookies() {
func add(item: Butter) {
print("Adding butter…")
}
func add(item: Flour) {
print("Adding flour…")
}
func add(item: Sugar) {
print("Adding sugar…")
}
add(item: Butter())
add(item: Flour())
add(item: 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.
Swift 5.4 also lets us call local functions before they are declared, meaning that we can now write code like this if needed:
func makeCookies2() {
add(item: Butter())
add(item: Flour())
add(item: Sugar())
func add(item: Butter) {
print("Adding butter…")
}
func add(item: Flour) {
print("Adding flour…")
}
func add(item: Sugar) {
print("Adding sugar…")
}
}
makeCookies2()
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 NonNegative<T: Numeric & Comparable> {
var value: T
var wrappedValue: T {
get { value }
set {
if newValue < 0 {
value = 0
} else {
value = newValue
}
}
}
init(wrappedValue: T) {
if wrappedValue < 0 {
self.value = 0
} else {
self.value = wrappedValue
}
}
}
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() {
@NonNegative var score = 0
// player was correct
score += 4
// player was correct again
score += 8
// player got one wrong
score -= 15
// player got another one wrong
score -= 16
print(score)
}
SE-0294 adds 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.
Swift 5.4 is available through Xcode 12.5, which is available from the Mac App Store.
Which features of Swift 5.4 are you most looking forward to?
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.