Master inout parameters, variadic functions, default values, and the performance tricks that separate professional iOS code from beginner mistakes
🫶 Quick thing before we dive in: If this article helps you even a little, tapping that 👏 button (you can hit it up to 50 times!) helps it reach more iOS devs. It's a small gesture that makes a big difference. Thanks!
So here's the thing… every iOS developer thinks they know functions. I mean, come on, it's just func, some parameters, maybe a return type, right?
Wrong.
Actually, let me rephrase that. You probably know the basics, but the devil's in the details. The way you handle parameter labels, default values, inout parameters — that's where you can spot a junior developer from across the room. It's not about being clever; it's about writing code that doesn't make your teammates want to throw their MacBook out the window.
I've been reviewing Swift code for years now, and trust me, the difference between "it works" and "it works beautifully" often comes down to how you design your function signatures.
📺 Watch the complete video tutorial on this topic — Coming soon to Swift Pal: https://youtube.com/@swift-pal
🎯 Why Function Parameters Actually Matter (More Than You Think)
When you're rushing to ship features, function parameters feel like the least of your worries. But here's what I've learned the hard way: bad function signatures create a ripple effect that haunts your codebase for months.
Bad parameter design leads to:
- Ambiguous function calls that require constant documentation lookups
- Performance issues from unnecessary copying
- Refactoring nightmares when requirements change
- Code that junior developers can't understand
The good news? Most of these problems are fixable with better parameter patterns. And once you know what to look for, it becomes second nature.🏷️ Parameter Labels vs Internal Names (Clarity Over Cleverness)
🏷️ Parameter Labels: The Art of Readable Function Calls
Here's where most developers get lazy. Swift gives you external parameter labels for a reason — use them!
// Bad: What does this do?
func process(_ data: Data, _ flag: Bool, _ count: Int) -> Result<String, Error>
let result = process(jsonData, true, 5)
// Good: Crystal clear at the call site
func parseJSON(from data: Data, allowFragments: Bool, maxDepth: Int) -> Result<String, Error>
let result = parseJSON(from: jsonData, allowFragments: true, maxDepth: 5)But don't go overboard. Sometimes omitting labels makes sense:
// This reads like English
func clamp(_ value: Double, to range: ClosedRange<Double>) -> Double {
min(max(value, range.lowerBound), range.upperBound)
}
let clamped = clamp(temperature, to: 0...100)The rule I follow: if someone reading the call site can't figure out what's happening without jumping to the function definition, you need better labels.
⚙️ Default Parameters: The Double-Edged Sword
Default parameters are awesome… until they're not. Here's what trips up most developers:
// This looks innocent
func fetchUser(id: String, timeout: TimeInterval = 5.0, retries: Int = 3) async throws -> User {
// Implementation here
}
// But what happens here?
await fetchUser(id: "123") // Uses default timeout and retries
await fetchUser(id: "456", timeout: 1.0) // Uses default retries onlyThe gotcha? Those defaults are evaluated every time they're used. So this is actually dangerous:
// DON'T DO THIS
func log(_ message: String, timestamp: Date = Date()) {
print("[\(timestamp)] \(message)")
}
log("Starting")
Thread.sleep(forTimeInterval: 1)
log("Finished")
// Both logs will have the SAME timestamp!Better approach:
func log(_ message: String, timestamp: Date? = nil) {
let time = timestamp ?? Date()
print("[\(time)] \(message)")
}🔄 inout Parameters: When You Actually Need Them
Okay, real talk — most developers either avoid inout completely or use it way too much. Here's when it actually makes sense.
Good use case: Genuinely need to modify the original
struct GameStats {
var score: Int = 0
var lives: Int = 3
var level: Int = 1
}
func levelUp(_ stats: inout GameStats) {
stats.level += 1
stats.score += 1000
stats.lives = min(stats.lives + 1, 5) // Cap at 5 lives
}
var playerStats = GameStats()
levelUp(&playerStats)
print(playerStats.level) // 2Bad use case: Just trying to avoid return values
// Don't do this
func calculateTotal(_ items: [Double], result: inout Double) {
result = items.reduce(0, +)
}
// Just return the value!
func calculateTotal(_ items: [Double]) -> Double {
items.reduce(0, +)
}Remember: inout is copy-in, copy-out. For large structs, it can actually be slower than just returning a new value, especially with Swift's copy-on-write optimizations.
📦 Variadic Parameters: Handle With Care
Variadic parameters look cool, but they come with hidden costs:
func average(_ numbers: Double...) -> Double {
guard !numbers.isEmpty else { return 0 }
return numbers.reduce(0, +) / Double(numbers.count)
}
// Nice syntax
let result = average(1.0, 2.0, 3.0, 4.0)But here's the catch: every call creates a new array. In performance-critical code, provide an array overload:
func average(_ numbers: [Double]) -> Double {
guard !numbers.isEmpty else { return 0 }
return numbers.reduce(0, +) / Double(numbers.count)
}
func average(_ numbers: Double...) -> Double {
average(numbers) // Delegates to array version
}
// Now you can use existing arrays efficiently
let data = [1.0, 2.0, 3.0, 4.0]
let result = average(data) // No extra allocation🚀 Advanced Patterns That Actually Matter
Multiple return values without tuples:
// Instead of returning a tuple
func parseResponse(_ data: Data) -> (success: Bool, message: String, code: Int) {
// ...
}
// Create a proper result type
struct APIResponse {
let success: Bool
let message: String
let code: Int
}
func parseResponse(_ data: Data) -> APIResponse {
// Much cleaner and extensible
}@autoclosure for lazy evaluation:
// Great for logging and assertions
func assert(_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
file: String = #file,
line: Int = #line) {
#if DEBUG
if !condition() {
print("Assertion failed: \(message()) at \(file):\(line)")
}
#endif
}
// The message is only evaluated if the assertion fails
assert(user.isValid, "User \(user.id) is invalid")⚡ Performance Gotchas You Should Know
- Default parameter evaluation
// This creates a new UUID every call, even with defaults!
func createUser(id: UUID = UUID(), name: String) -> User {
User(id: id, name: name)
}
// Better: make it optional
func createUser(id: UUID? = nil, name: String) -> User {
User(id: id ?? UUID(), name: name)
}2. Large struct copying
struct MassiveDataSet {
let data: [Double] // Imagine this has 10,000 elements
}
// This copies the entire struct twice (in and out)
func process(_ dataset: inout MassiveDataSet) {
// Modify dataset
}
// Often better to return a new one
func process(_ dataset: MassiveDataSet) -> MassiveDataSet {
// Swift's copy-on-write might make this faster
}3. Closure capture overhead
class DataProcessor {
var results: [String] = []
// This captures self strongly
func processAsync(completion: @escaping ([String]) -> Void) {
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
completion(self.results)
}
}
}🎯 Key Takeaways (The Stuff You'll Actually Remember)
- Parameter labels are documentation — make call sites readable
- Default parameters are evaluated every time — keep them simple
- Use
inoutsparingly — only when you truly need mutation - Provide array overloads for variadic functions — performance matters
- Design signatures for the call site — not just the implementation
- Return proper types, not tuples — your future self will thank you
Look, I've seen codebases where every function call requires a trip to the documentation just to figure out what the parameters do. Don't be that developer. Spend the extra minute thinking about your function signatures — it's an investment that pays dividends every time someone (including you) uses that function.
📺 Watch the complete video tutorial on this topic — Coming soon to Swift Pal: https://youtube.com/@swift-pal
The next time you're about to write func doSomething(_ a: Int, _ b: String, _ c: Bool), stop. Think about the poor developer who's going to call this function at 2 AM while trying to fix a production bug. Make their life easier.
🎉 Enjoyed this article? Your support means the world to me!
👏 Give it some claps if it helped you out — it really helps other developers discover this content
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
🐦 Follow me on Twitter for daily SwiftUI tips and tricks: https://twitter.com/swift_pal
💼 Let's connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
📬 Subscribe to my Medium so you never miss my latest deep dives: https://medium.com/@karan.pal/subscribe
☕ Buy me a coffee if this article helped you (seriously, your support keeps me writing!): https://coff.ee/karanpaledx
Happy coding! 🚀