Swift and SwiftUI for Web Developers Labels
In part 1 and part 2 of this series, we learned how to create functions and closures. To reiterate, here’s what a simple function to load all friends for a specific user could look like:
func loadFriends(userID: String, addedAfter: Date) async -> [Friend] {
// Implementation
}
TypeScript Functions
Let’s start with how you’d design the API for a simple function in TypeScript. The simplest version of this could look something like this:
const loadFriends = async (
userId: string,
addedAfter: Date
): Promise<Friend[]> => {
// Implementation
}
If you defined the function like this in TypeScript, here’s what the invocation of the function would look like:
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const newFriends = await loadFriends('3f17a', yesterday)
This is fine, but it poses a few problems:
- Just from the function call, it’s not clear what these arguments are for.
- If we want to add arguments later on and make some arguments (like
addedAfter
) optional, we can’t with this API design.
Typically, in TypeScript, this is solved by accepting an object as an argument instead:
const loadFriends = async ({
userId,
addedAfter
}: {
userId: string
addedAfter: Date
}): Promise<Friend[]> => {
// Implementation
}
const newFriends = await loadFriends({
userId: '3f17a',
addedAfter: yesterday
})
This makes the call site much more readable. It also opens the door for any
future arguments we might want to add, with the possibility of making
addedAfter
optional. But it comes at the cost of making the function
definition bulkier, repetitive, and harder to read.
The Swift Way
Now, let’s revisit our initial Swift example:
func loadFriends(userID: String, addedAfter: Date) async -> [Friend] {
// Implementation
}
This is how you would need to call this function in Swift:
let yesterday = Date().addingTimeInterval(-60 * 60 * 24)
let friends = await loadFriends(userID: "3f17a", addedAfter: yesterday)
Swift requires you to specify the parameter names in the function invocation. This is because every input parameter of a function has a label, and by default, Swift uses the name of the parameter as the label.
However, you can change that behavior by defining your own labels:
func loadFriends(
for userID: String,
since addedAfter: Date
) async -> [Friend] {
// Here, the variables userID and addedAfter can be used.
}
let friends = await loadFriends(for: "3f17a", since: yesterday)
Great! You can see that I’ve also changed addedAfter
to since
. Swift labels
allow you to write really elegant function signatures that read like natural
language: load [the] friends for [the user id] "3f17a" since yesterday
.
Inside the function, using the variable names for
and since
as actual
variables wouldn’t be pragmatic.
This is one reason why (most) Swift APIs are such a pleasure to read.
forId
?
Shouldn’t it be You might wonder whether forId
would be a better label than for
in this
instance. After all, what if you want to add support for passing a User
object
to the function later on?
In TypeScript, you could support this by overloading the function or setting the
for
type to string | User
. However, it would be up to you, the implementer
of the function, to check which one of the two possible types has been passed,
leading to potentially annoying and error-prone code.
Swift supports proper function overloading. This means that adding support for another type is as simple as:
func loadFriends(
for user: User,
since addedAfter: Date
) async -> [Friend] {
loadFriends(for: user.id, since: addedAfter)
}
No Labels
Sometimes it makes sense to not use a label at all. In that case, you can define
a parameter with the _
label. If you wanted to remove the label for the user
altogether, you would do it like this:
func loadUserFriends(
_ userID: String,
since addedAfter: Date
) async -> [Friend] {
// Implementation
}
let friends = await loadUserFriends("3f17a", since: yesterday)
The _
character is often used in Swift to express the intent of deliberately
not using something, for example:
// This will trigger a warning if `result` is never used.
let result = await fetchData()
// This will trigger a warning that `fetchDate` returns a
// result that is not used.
await fetchData()
// This lets Swift know that you are discarding the result
// of fetchData on purpose.
let _ = await fetchData()
Conclusion
I hope you’re as excited as I am about how labels can make our APIs look clean and easy to understand. They push us to think carefully about how we name our functions and what we call our variables inside these functions. This approach is not just intuitive; it also makes coding more straightforward and efficient, since it focuses on making our code clear and easy to read right from the start.
Need a Break?
I built Pausly to help people like us step away from the screen for just a few minutes and move in ways that refresh both body and mind. Whether you’re coding, designing, or writing, a quick break can make all the difference.
Give it a try