Put Your Defaults at the End: Argument Ordering in Swift
There's a small decision you make every time you write a function signature: what order should the parameters go in?
For most functions it's obvious — the important stuff comes first. But when default values enter the picture, things get a bit more interesting. And getting it wrong can make your API awkward to use.
The Golden Rule
Swift's API Design Guidelines spell it out clearly:
"Prefer to locate parameters with defaults toward the end of the parameter list. Parameters without defaults are usually more essential to the semantics of a method, and provide a stable initial pattern of use where methods are invoked."
In plain English: required parameters first, optional (defaulted) parameters last.
Let's see why this matters.
The Problem with Defaults in the Middle
Imagine you're writing a function to fetch a user:
// ❌ Defaults scattered around
func fetchUser(
useCache: Bool = true,
id: String,
includeMetadata: Bool = false
) async throws -> User
Now, Swift does let you skip defaulted parameters — you can call this as:
let user = try await fetchUser(id: "123")
So what's the problem? Look at the signature again. The parameter that matters most — the user ID — is sandwiched between configuration options. When someone reads this function declaration, they have to hunt for the essential information.
And at the call site, the "shape" of the call changes depending on which defaults you override:
fetchUser(id: "123")
fetchUser(useCache: false, id: "123")
fetchUser(id: "123", includeMetadata: true)
fetchUser(useCache: false, id: "123", includeMetadata: true)
The id parameter jumps around. Sometimes it's first, sometimes it's second. That inconsistency makes the code harder to scan.
The Fix: Required First, Defaults Last
// ✅ Required first, defaults at the end
func fetchUser(
id: String,
useCache: Bool = true,
includeMetadata: Bool = false
) async throws -> User
Now every call starts the same way:
fetchUser(id: "123")
fetchUser(id: "123", useCache: false)
fetchUser(id: "123", includeMetadata: true)
fetchUser(id: "123", useCache: false, includeMetadata: true)
The essential information — the user ID — is always first. The optional configuration follows. The call site has a consistent structure regardless of which defaults you override.
A Real-World Example
Here's a pattern you'll see in networking code:
// ❌ Required params buried in the middle
func request(
timeout: TimeInterval = 30,
method: HTTPMethod,
url: URL,
headers: [String: String] = [:],
body: Data? = nil
) async throws -> Response
The two things you must specify — the method and URL — are surrounded by optional configuration. The signature doesn't communicate what's essential at a glance.
Compare:
// ✅ Required first, config at the end
func request(
method: HTTPMethod,
url: URL,
headers: [String: String] = [:],
body: Data? = nil,
timeout: TimeInterval = 30
) async throws -> Response
Now the signature tells a clear story: "To make a request, you need a method and a URL. Everything else is optional."
The simple case reads simply:
let response = try await request(method: .get, url: myURL)
And the complex case builds naturally from there:
let response = try await request(
method: .post,
url: apiURL,
headers: ["Authorization": "Bearer \(token)"],
body: jsonData,
timeout: 60
)
Why This Works
When you put required parameters first:
The signature communicates intent. A quick glance tells you what's essential and what's optional. Required parameters are the "what", defaults are the "how".
Call sites have a consistent shape. The required arguments always appear first, in the same order. You can scan a codebase and quickly spot patterns.
The signature is stable. If you add a new optional parameter with a default, existing call sites don't change. They just keep working, unaware of the new option tucked away at the end.
Autocomplete flows naturally. When you're typing the function call, you're prompted for the essential parameters first. The optional stuff appears at the end, clearly marked with their default values.
What About Related Parameters?
Sometimes logical grouping trumps the strict "required first" rule. If two parameters are conceptually linked, keep them together:
func drawRect(
at origin: CGPoint, // Required: where
size: CGSize, // Required: how big
cornerRadius: CGFloat = 0, // Optional: styling (related to shape)
fillColor: Color = .clear, // Optional: styling
strokeColor: Color = .black // Optional: styling
)
The required parameters (origin, size) define what you're drawing. The optional parameters define how it looks. Grouping makes sense here, even though you could argue cornerRadius "relates to" the shape's dimensions.
Use your judgement. The goal is readability, not rigid adherence to a rule.
The Exception: Trailing Closures
Swift has special syntax for trailing closures, so closure parameters typically go last regardless of whether they have defaults:
func animate(
duration: TimeInterval = 0.3,
delay: TimeInterval = 0,
options: AnimationOptions = [],
animations: () -> Void
)
// Called as:
animate {
view.alpha = 0
}
// Or with options:
animate(duration: 0.5, delay: 0.2) {
view.alpha = 0
}
Here, animations is required but goes last to enable the trailing closure syntax. That's fine — the readability gain from trailing closures outweighs the "required first" principle.
Quick Checklist
When ordering your function parameters:
- Required parameters first — these are the essential inputs
- Optional parameters (with defaults) last — these are configuration
- Group related parameters together — even if it bends the rule slightly
- Closures at the very end — to enable trailing closure syntax
Wrapping Up
Parameter ordering is a small thing, but small things compound. A well-ordered function signature makes the common case easy and the complex case possible. A poorly-ordered one forces everyone to think harder than they should.
Put the important stuff first. Put the "nice to have" stuff last. Your callers will thank you.