Swift Functions — Parameters, Return Values, and Closures Explained
In this tutorial, you will learn about Swift Functions. We cover key concepts, practical examples, and best practices to help you master this topic.
Swift functions are first-class citizens that can be assigned to variables, passed as arguments, returned from other functions, and captured in closures, with features like argument labels, default parameter values, variadic parameters, in-out parameters, and throwing functions for error propagation. This tutorial covers defining and calling functions, using external and internal parameter names, creating closures with shorthand syntax, escaping closures, and Functional Programming patterns.
What You'll Learn
- Defining functions with parameters and return types
- Using argument labels for readable call sites
- Setting default parameter values
- Creating variadic functions with variable argument counts
- Modifying parameters with in-out
- Defining throwing functions and rethrowing
- Writing closures with shorthand argument names
- Using trailing closure syntax and escaping closures
- Autoclosures for deferred execution
Why It Matters
Functions are the primary way to organize code in Swift. Swift's parameter system encourages readable call sites through argument labels. Closures enable functional programming patterns like map, filter, and sort with concise syntax. Understanding function types and closures is essential for working with SwiftUI's view Builder pattern, Combine's publishers, and Grand Central Dispatch.
Real-World Use
The Doda Browser networking layer uses throwing functions for API calls, escaping closures for completion handlers, and trailing closure syntax for URLSession tasks. The UI layer uses closures extensively in SwiftUI's onTapGesture, onChange, and task modifiers.
Learning Path
flowchart LR A[Control Flow] --> B[Functions\nYou are here] B --> C[Optionals] style B fill:#f90,color:#fff
Basic Functions
Functions are defined with func, parameters with names and types, and return types with ->:
import Foundation
// Function with no parameters and no return
func greet() {
print("Hello, world!")
}
// Function with parameters and return value
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
// Function with single expression return (implicit)
func multiply(_ a: Int, _ b: Int) -> Int { a * b }
greet()
print("Add: \(add(5, 3))")
print("Multiply: \(multiply(5, 3))")
Output:
Hello, world!
Add: 8
Multiply: 15
Argument Labels
Swift distinguishes between external (call site) and internal (function body) parameter names:
import Foundation
// Both external and internal labels
func sendMessage(to recipient: String, withBody body: String) {
print("Sending message to \(recipient): \(body)")
}
// Omitting external label with _
func calculate(_ value: Int, multipliedBy factor: Int) -> Int {
return value * factor
}
// External label only (Swift creates internal name matching external)
func greet(person name: String) {
print("Hello, \(name)!")
}
sendMessage(to: "Alice", withBody: "Meeting at 3pm")
print("Result: \(calculate(10, multipliedBy: 3))")
greet(person: "Bob")
Output:
Sending message to Alice: Meeting at 3pm
Result: 30
Hello, Bob!
Default Parameters and Variadic Functions
Default values make functions flexible without overloading:
import Foundation
// Default parameter values
func createUser(name: String, age: Int = 18, isAdmin: Bool = false) -> String {
let role = isAdmin ? "admin" : "user"
return "\(name) (\(age), \(role))"
}
print(createUser(name: "Alice", age: 30, isAdmin: true))
print(createUser(name: "Bob")) // Uses defaults
print(createUser(name: "Charlie", age: 25))
// Variadic parameters (zero or more values)
func average(_ numbers: Double...) -> Double {
if numbers.isEmpty { return 0 }
return numbers.reduce(0, +) / Double(numbers.count)
}
print("Average: \(average(1, 2, 3, 4, 5))")
print("Average: \(average(10.5, 20.3))")
print("Average: \(average())")
Output:
Alice (30, admin)
Bob (18, user)
Charlie (25, user)
Average: 3.0
Average: 15.4
Average: 0.0
In-Out Parameters
In-out parameters allow functions to modify external variables:
import Foundation
func swapValues(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
print("Before swap: x = \(x), y = \(y)")
swapValues(&x, &y)
print("After swap: x = \(x), y = \(y)")
// Practical example: increment
func increment(_ value: inout Int, by amount: Int = 1) {
value += amount
}
var counter = 0
increment(&counter)
print("Counter after increment: \(counter)")
increment(&counter, by: 10)
print("Counter after increment by 10: \(counter)")
Output:
Before swap: x = 10, y = 20
After swap: x = 20, y = 10
Counter after increment: 1
Counter after increment by 10: 11
Throwing Functions
Functions can throw errors for structured error handling:
import Foundation
enum DivisionError: Error {
case divisionByZero
case overflow
}
func safeDivide(_ a: Double, _ b: Double) throws -> Double {
guard b != 0 else {
throw DivisionError.divisionByZero
}
guard a.isFinite && b.isFinite else {
throw DivisionError.overflow
}
return a / b
}
// Handling throws with do-catch
do {
let result = try safeDivide(10, 2)
print("Result: \(result)")
let invalidResult = try safeDivide(5, 0)
print("This will not print")
} catch DivisionError.divisionByZero {
print("Error: Cannot divide by zero")
} catch {
print("Unexpected error: \(error)")
}
// Using try? to convert to optional
if let result = try? safeDivide(8, 4) {
print("Optional result: \(result)")
}
Output:
Result: 5.0
Error: Cannot divide by zero
Optional result: 2.0
Closures
Closures are self-contained blocks of functionality:
import Foundation
// Closure syntax
let greeting: (String) -> String = { name in
return "Hello, \(name)!"
}
print(greeting("Alice"))
// Shorthand argument names
let squared: (Int) -> Int = { $0 * $0 }
print("Squared: \(squared(5))")
// Trailing closure syntax
let numbers = [3, 1, 5, 2, 4]
let sorted = numbers.sorted { $0 < $1 }
print("Sorted: \(sorted)")
// Closure as a parameter
func performOperation(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int {
return operation(a, b)
}
let sum = performOperation(10, 20) { $0 + $1 }
let product = performOperation(10, 20) { $0 * $1 }
print("Sum: \(sum), Product: \(product)")
// Capturing values
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter1 = makeCounter()
print("Counter 1: \(counter1())")
print("Counter 1: \(counter1())")
let counter2 = makeCounter()
print("Counter 2: \(counter2())")
Output:
Hello, Alice!
Squared: 25
Sorted: [1, 2, 3, 4, 5]
Sum: 30, Product: 200
Counter 1: 1
Counter 1: 2
Counter 2: 1
Common Mistakes
- Forgetting argument labels at call sites: Every parameter has an external label by default. Use
_to omit it. - Strong reference cycles in closures: Closures that capture self strongly can cause retain cycles. Use
[weak self]or[unowned self]in capture lists. - Not marking functions as throwing: Functions that call throwing functions must be marked with
throwsor handle the error with do-catch. - Overloading by return type only: Swift does not allow two functions with the same name and parameter types but different return types.
- Using closures when a function is clearer: For reusable operations, define a function. Use closures for short, context-specific operations.
Practice Questions
What is the difference between argument labels and parameter names?
- Argument labels are used at the call site. Parameter names are used inside the function body. They can be the same or different.
How does in-out work in Swift?
- in-out parameters are passed by reference. The caller uses
&prefix. The function can modify the original variable.
- in-out parameters are passed by reference. The caller uses
What is trailing closure syntax?
- If the last parameter is a closure, you can write it outside the parentheses.
funcName { closure body }.
- If the last parameter is a closure, you can write it outside the parentheses.
What does
@escapingmean for a closure parameter?- The closure outlives the function scope (e.g., stored for later use). Non-escaping closures execute within the function body.
Challenge: Write a function
applythat takes an array of Int and a closure (Int -> Int) and returns a new array with the closure applied to each element. Then use it with an inline closure that doubles each value.
Mini Project
Build a functional array processor:
- Write a
composefunction that takes two closures and returns their composition. - Write a
partialfunction that fixes some arguments of a multi-parameter function. - Write a
curryfunction that converts a two-parameter function into a curried version. - Use these with map, filter, and reduce to demonstrate functional composition.
FAQ
What's Next
Learn how to handle missing values safely in the Optionals tutorial.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro