So far we've been passing closures into functions that Swift provides โ like sorted(), filter(), and map(). But what if you wanted to write your own function that accepts another function as a parameter? That's exactly what we're covering today. ๐ฅ
This might sound complicated, but once you see it in action it starts to make a lot of sense โ and it's everywhere in SwiftUI.
Why Would You Even Want This?
Before we write any code, let's think about why this matters.
Imagine you're building an anime battle app that needs to fetch data from a server โ maybe pulling in a list of all One Piece characters. Your iPhone can do billions of things per second, but waiting for a server response can take half a second or more. That's practically glacial by comparison.
If your app just sat there waiting for the server to respond, the whole UI would freeze. Nobody wants that.
The solution? Pass in a closure that says: "go do this slow work, and when you're done, call this function with the result." Your app keeps running smoothly, and the closure fires when the data arrives. That's closures as parameters in a real-world nutshell. ๐
Writing a Function That Accepts a Function
Let's start with a practical example. Here's a function that generates an array of random jutsu power levels by calling another function repeatedly:
func generatePowerLevels(count: Int, using generator: () -> Int) -> [Int] {
var levels = [Int]()
for _ in 0..<count {
let newLevel = generator()
levels.append(newLevel)
}
return levels
}
Let's break down what's happening on that first line:
func generatePowerLevels(count: Int, using generator: () -> Int) -> [Int]
-
count: Intโ how many power levels to generate -
using generator: () -> Intโ a function parameter calledgenerator, which takes no parameters itself but returns anIntevery time it's called -
-> [Int]โ the wholegeneratePowerLevelsfunction returns an array of integers
Inside the function, we just call generator() on each loop iteration, collecting its returned value into our array.
Now let's call it using a trailing closure:
let powerLevels = generatePowerLevels(count: 5) {
Int.random(in: 1...9000)
}
print(powerLevels) // e.g. [4521, 8832, 312, 7741, 999]
Swift sees the trailing closure and knows it matches the generator: () -> Int parameter โ no labels needed.
You can also pass in a named function instead of a closure:
func randomChakra() -> Int {
Int.random(in: 1...9000)
}
let chakraLevels = generatePowerLevels(count: 5, using: randomChakra)
print(chakraLevels)
Both produce exactly the same result โ a closure and a named function are interchangeable here, because they have the same type: () -> Int.
Reading the Function Signature
The trickiest part of accepting functions as parameters is reading the syntax. Let's slow down on it:
func generatePowerLevels(count: Int, using generator: () -> Int) -> [Int]
There are two -> arrows here, which can be confusing at first:
- The first
->(inside() -> Int) belongs to the parameter function โ it describes what the function we're passing in returns - The second
->(at the end,-> [Int]) belongs to our function โ it describes whatgeneratePowerLevelsitself returns
Think of it like this: generator is a function that lives inside the parameter list. It has its own type, just like Int or String would โ it just happens to be a function type.
Multiple Trailing Closures
Here's something that appears constantly in SwiftUI: functions that accept multiple function parameters.
Imagine an anime training sequence that needs to run three stages โ warmup, training, and cool-down โ each customizable:
func runTrainingArc(warmup: () -> Void, training: () -> Void, cooldown: () -> Void) {
print("๐ Starting warmup...")
warmup()
print("โ๏ธ Starting training...")
training()
print("๐ง Starting cooldown...")
cooldown()
print("โ
Training arc complete!")
}
When calling a function with multiple trailing closures, the first one works exactly like before โ no label, just {. But the second and third each get their label written outside the brace:
runTrainingArc {
print("Stretching and light jogging")
} training: {
print("Naruto Shadow Clone Jutsu x1000")
} cooldown: {
print("Ramen break ๐")
}
Output:
๐ Starting warmup...
Stretching and light jogging
โ๏ธ Starting training...
Naruto Shadow Clone Jutsu x1000
๐ง Starting cooldown...
Ramen break ๐
โ
Training arc complete!
This multiple-trailing-closure syntax is something you'll see all the time in SwiftUI โ for example, creating a Section with a header, content, and footer each uses a separate trailing closure.
The Function Type Cheat Sheet
When writing function parameters, here's how to read and write the types:
| What the function does | Its type |
|---|---|
| Takes nothing, returns nothing | () -> Void |
| Takes nothing, returns an Int | () -> Int |
| Takes a String, returns nothing | (String) -> Void |
| Takes two Strings, returns a Bool | (String, String) -> Bool |
The parameter list goes in the first (), and the return type goes after ->. That's all function types are โ a description of what goes in and what comes out.
Why This Matters for SwiftUI
In SwiftUI, almost every component you build uses this pattern. A Button accepts a function for what happens when tapped, and a function for what to display. A List accepts a function to generate each row. Even a VStack uses a closure to hold all its child views.
Once you're comfortable with the idea of passing functions around as arguments, SwiftUI's syntax stops looking mysterious and starts reading like a natural description of your UI. That's the payoff for all this closure work. ๐ธ
Wrap Up
| Concept | What It Means |
|---|---|
| Function as parameter | You can pass a function (or closure) as an argument to another function |
() -> Int |
A function type โ takes nothing, returns an Int |
| Multiple trailing closures | Call each extra closure with its label outside the brace |
| Named function vs closure | Interchangeable as long as the type matches |
This article was written by me; AI was used to improve grammar and readability.
Top comments (0)