Skip to content

Swift Functions — Parameters, Return Values, and Closures Explained

DodaTech Updated 2026-06-28 7 min read

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

  1. Forgetting argument labels at call sites: Every parameter has an external label by default. Use _ to omit it.
  2. Strong reference cycles in closures: Closures that capture self strongly can cause retain cycles. Use [weak self] or [unowned self] in capture lists.
  3. Not marking functions as throwing: Functions that call throwing functions must be marked with throws or handle the error with do-catch.
  4. Overloading by return type only: Swift does not allow two functions with the same name and parameter types but different return types.
  5. Using closures when a function is clearer: For reusable operations, define a function. Use closures for short, context-specific operations.

Practice Questions

  1. 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.
  2. 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.
  3. What is trailing closure syntax?

    • If the last parameter is a closure, you can write it outside the parentheses. funcName { closure body }.
  4. What does @escaping mean for a closure parameter?

    • The closure outlives the function scope (e.g., stored for later use). Non-escaping closures execute within the function body.
  5. Challenge: Write a function apply that 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 compose function that takes two closures and returns their composition.
  • Write a partial function that fixes some arguments of a multi-parameter function.
  • Write a curry function that converts a two-parameter function into a curried version.
  • Use these with map, filter, and reduce to demonstrate functional composition.

FAQ

Can functions return multiple values?

Yes. Use tuples: func getStats() -> (min: Int, max: Int, avg: Double). The caller accesses values by name or position.

What is the difference between a function and a closure?

Functions are named closures. Closures are unnamed function-like blocks that can capture values from their surrounding scope.

Can I nest functions in Swift?

Yes. Inner functions are only visible within the outer function's scope. They can capture values from the enclosing function.

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