Swift and SwiftUI tutorials for Swift Developers

Functions in Swift – Tutorial with examples

In the vast universe of software development, regardless of the language you choose, there exists a fundamental atom, an indivisible unit of logic that breathes life into applications: the function.

If you are learning Swift, Apple’s modern and powerful language, you may have noticed that it is an “opinionated” language. It has a very specific way of doing things, prioritizing readability and safety. Functions in Swift are not just blocks of code; they are “first-class citizens” that can be passed around as variables, transform data, and structure complex architectures.

In this deep-dive tutorial, we will break down what a function really is in Swift, how its syntax differs from other C-style languages, and how you can use them to write modular, testable, and scalable code.


1. What is a function, really?

Before writing code, let’s define the concept. A function is a self-contained block of code that performs a specific task.

Imagine a coffee machine.

  1. Input: You give it coffee beans and water.
  2. Processing: The machine grinds, heats, and filters.
  3. Output: It gives you a cup of hot coffee.

In programming, this translates directly:

  • The function receives parameters (beans and water).
  • It executes a body of code (the internal mechanism).
  • It returns a return value (the cup of coffee).

In Swift, functions are extremely flexible. They don’t need to have inputs, nor do they even need to return a visible output.


2. Basic Syntax: Your First Function

Swift uses the func keyword to declare functions. Unlike languages like Java or C++, where the return type goes at the beginning, Swift uses an arrow -> to indicate what will come out of the function, improving left-to-right readability.

The Anatomical Structure

func functionName(parameter: Type) -> ReturnType {
    // Function body
    return value
}

Let’s look at the simplest possible example: a function that just prints a message.

func greet() {
    print("Hello, Swift world!")
}

// To execute it, we "call" it:
greet()

Here there are no parameters (the parentheses are empty) and no return arrow (it implicitly returns Void, or empty).


3. Parameters and Arguments: The Art of Communication

This is where Swift shines and stands out from the competition. Swift inherits from Objective-C the philosophy that code should read like an English sentence.

To achieve this, Swift distinguishes between:

  1. Parameter Name: Used inside the function.
  2. Argument Label: Used outside the function, when calling it.

3.1. Standard Parameters

func square(number: Int) -> Int {
    return number * number
}

let result = square(number: 5) // Reads: "square number 5"

3.2. Custom Argument Labels

Sometimes, square(number: 5) sounds redundant. Or in other cases, we want the call to be more fluid. We can define an external label different from the internal name.

// 'to' is the external label (for the caller)
// 'person' is the internal name (for the code inside the function)
func sayHello(to person: String) {
    print("Hello, \(person)!")
}

// The call reads like a natural sentence:
sayHello(to: "Maria")

If we had used only person, the call would be sayHello(person: "Maria"), which is more technical and less narrative.

3.3. Omitting Labels (_)

Sometimes, the context is so obvious that we don’t need any external label. We use the underscore _ to tell Swift: “ignore the external label.”

func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

// Now it looks like a pure mathematical operation
let sum = add(10, 5)

3.4. Default Values

What if a parameter is almost always the same? Instead of forcing the user to type it every time, we can give it a default value. This is vital for keeping APIs clean.

func createButton(title: String, color: String = "Blue") {
    print("Creating button \(title) with color \(color)")
}

createButton(title: "OK") // Uses "Blue" automatically
createButton(title: "Cancel", color: "Red") // Overwrites with "Red"

This avoids what is known in other languages as “Constructor Overloading” or having multiple functions with the same name but different parameters just to handle default options.


4. Return Values: Beyond Simple Data

Swift is a strongly typed language. If you say you return an Int, you must return an Int. But Swift offers powerful tools to handle returns.

4.1. Tuples: Returning Multiple Values

In older languages, if you wanted to return two things (e.g., X and Y coordinates), you had to create a class, a struct, or use a confusing array. In Swift, we use Tuples.

func getGPSCoordinates() -> (latitude: Double, longitude: Double) {
    // Simulating a reading
    return (40.4168, -3.7038)
}

let position = getGPSCoordinates()

// We can access by name
print("You are at latitude: \(position.latitude)")

Tuples are ideal for lightweight, temporary groups of data that don’t require the formality of a full structure.

4.2. Optionals: When the Function Can Fail

Sometimes, a function cannot do its job. Imagine a function that searches for a user by ID in a database. What happens if the ID doesn’t exist?

We shouldn’t return an empty user or a generic error. We must return nil. For that, the return type is marked with a question mark ?

func findUser(id: Int) -> String? {
    let database = [1: "Ana", 2: "Carlos"]
    
    // If it finds the id it returns the name, otherwise it returns nil
    return database[id]
}

// Safe handling of the return
if let user = findUser(id: 99) {
    print("User found: \(user)")
} else {
    print("User does not exist")
}

5. Variadic Parameters

Sometimes you don’t know how many parameters will be sent to you. Do you want to add 2 numbers? Or 20? Swift allows accepting an indefinite number of parameters of the same type using three dots ....

func average(_ numbers: Double...) -> Double {
    // Inside the function, 'numbers' behaves like an Array [Double]
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}

print(average(5, 10, 15, 20)) // Works with 4 arguments
print(average(2, 4))          // Works with 2 arguments

This is how Swift’s print() function works, which can receive as many strings as you want separated by commas.


6. In-Out Functions: Modifying Reality

By default, parameters in Swift are constants (let). If you try to change the value of a parameter inside the function, Swift will give you a compilation error. This is for safety: it avoids unwanted side effects.

But what if you need to modify the original variable that was passed to you? We use the keyword inout.

func swapValues(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

var x = 10
var y = 30

// We must use '&' (ampersand) to indicate we are passing the reference
swapValues(&x, &y)

print("x is now \(x), y is now \(y)") // x: 30, y: 10

Technical Note: This is known as “Copy-in Copy-out” or call by value-result, although for practical purposes it behaves similarly to pass-by-reference in C++.


7. Function Types: Functions as First-Class Citizens

This is a concept that separates novice programmers from advanced ones. In Swift, a function is a data type, just like an Int or a String.

This means you can:

  1. Assign a function to a variable.
  2. Pass a function as a parameter to another function.
  3. Return a function from another function.

The Function Type

The “type” of a function is defined by its parameters and its return. Example: A function that takes two Ints and returns an Int is of type (Int, Int) -> Int.

func add(a: Int, b: Int) -> Int { return a + b }
func multiply(a: Int, b: Int) -> Int { return a * b }

// Variable that stores A FUNCTION
var mathOperation: (Int, Int) -> Int

// We assign the add function
mathOperation = add
print(mathOperation(4, 2)) // Prints 6

// We change to multiply dynamically
mathOperation = multiply
print(mathOperation(4, 2)) // Prints 8

This concept is the foundation of Functional Programming and is essential for understanding SwiftUI tools or array handling like mapfilter, and reduce.


8. Best Practices and Clean Code in Functions

Knowing the syntax is 50%. The other 50% is knowing how to write functions that your teammates (or yourself in 6 months) can understand.

8.1. Single Responsibility Principle (SRP)

A function should do one thing only and do it well.

  • Bad: func processDataAndSaveToDBAndUpdateUI()
  • Good: func processData()func save()func updateUI()

If your function is longer than 20 or 30 lines, it’s a red flag. It’s probably doing too much. Break it down.

8.2. Naming Convention

Names should be verbs if they perform an action (calculateTotaldownloadImage) or nouns if they return a value without side effects (distance(to:point)).

In Swift, clarity takes precedence over brevity. Don’t be afraid of long names if they are descriptive.

8.3. Guard Statements (Early Exit)

To avoid the “Pyramid of Doom” (nested braces hell), use guard to validate preconditions at the start of the function.

func divide(dividend: Double, divisor: Double) -> Double? {
    // Validation first
    guard divisor != 0 else {
        print("Error: Cannot divide by zero")
        return nil
    }
    
    // If it passes the guard, the code continues cleanly, without being inside an 'else'
    return dividend / divisor
}

9. Nested Functions

Sometimes, you have a helper function that is only useful inside another specific function. To avoid “polluting” the global scope of your code, you can define functions inside functions.

func spaceRace(progress: Int) {
    
    // This function only exists inside 'spaceRace'
    func analyzeFuel() {
        print("Checking levels...")
    }
    
    analyzeFuel()
    print("Launching rocket at \(progress)%")
}

This encapsulates logic and keeps your code organized.


10. Conclusion

Functions in Swift are a tool of precision engineering. The combination of argument labels (for human readability), tuples (for data flexibility), and their nature as “First-Class Citizens” (for functional power), make working with them a pleasure.

Summary of what we learned:

  1. Use func to declare.
  2. Use -> for the return.
  3. Argument labels (tofrom_) make your code read like English.
  4. Default values clean your code of repetition.
  5. Functions are data types; use them as variables to create modular code.

What is the next step?

Now that you master the structure, the next logical step is to learn about Closures. Closures are, essentially, unnamed functions that can be passed around as compact code. They are the heart of asynchrony and event handling in Swift and SwiftUI.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to learn to code in SwiftUI

Next Article

SwiftUI Toolbar - Tutorial with Examples

Related Posts