Swift and SwiftUI for Web Developers Labels

This is the 3rd post in a series about Swift and SwiftUI for web developers, aimed at highlighting the beautiful aspects of the language. To learn more about Swift, refer to the official documentation.

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:

  1. Just from the function call, it’s not clear what these arguments are for.
  2. 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.

Shouldn’t it be forId?

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
Pausly movement