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 only

The 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)  // 2

Bad 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

  1. 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)

  1. Parameter labels are documentation — make call sites readable
  2. Default parameters are evaluated every time — keep them simple
  3. Use inout sparingly — only when you truly need mutation
  4. Provide array overloads for variadic functions — performance matters
  5. Design signatures for the call site — not just the implementation
  6. 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! 🚀