Passing closure as an argument while initializing an object in Swift and SwiftUI

·

6 min read

In Swift, closures are self-contained blocks of functionality that can be passed around and used in your code. They can be treated like any other type, which means they can also be passed as an argument while initializing an object.

Here's a simple example:

class SomeClass {
    let closure: () -> Void

    init(closure: @escaping () -> Void) {
        self.closure = closure
    }

    func executeClosure() {
        closure()
    }
}

// Here we're passing a closure that prints "Hello, World!" when executed
let instance = SomeClass(closure: {
    print("Hello, World!")
})

instance.executeClosure()  // This will print "Hello, World!"

In this example, we define a class SomeClass with a property closure of type () -> Void (a closure that takes no parameters and returns Void). SomeClass has an initializer that accepts a closure of the same type.

We can then initialize an instance of SomeClass providing a closure that prints "Hello, World!" when called. Finally, we call executeClosure() on the instance to execute the stored closure.

A practical example in Swift

Let's consider a class called Car which receives a closure in its initializer. This closure called statusReport will receive an integer fuelAmount as input and return a string message.

// Define a Car class
class Car {
    let statusReport: (Int) -> String

    var fuelAmount: Int {
        didSet {
            print(self.statusReport(fuelAmount))
        }
    }

    init(fuelAmount: Int, statusReport: @escaping (Int) -> String) {
        self.fuelAmount = fuelAmount
        self.statusReport = statusReport
    }
}

// Initialize a Car instance by passing in a closure
let myCar = Car(fuelAmount: 60, statusReport: { fuelAmount in
    if fuelAmount < 20 {
        return "Low on fuel. Please refill."
    } else if fuelAmount >= 20 && fuelAmount < 60 {
        return "Adequate fuel."
    } else {
        return "Full tank. Ready to go!"
    }
})

myCar.fuelAmount = 50 // Prints: "Adequate fuel."
myCar.fuelAmount = 10 // Prints: "Low on fuel. Please refill."
myCar.fuelAmount = 80 // Prints: "Full tank. Ready to go!"

In this example, we are creating a Car class. When initializing a Car instance, we pass a closure statusReport that accepts an integer fuelAmount as a parameter and returns a string that represents a status message depending on the amount of fuel. Every time the fuel level changes, the statusReport closure is called, and the corresponding status message is printed.

A practical example in SwiftUI syntax

Let's consider a SwiftUI View that receives a closure when it's initialized. This is common in SwiftUI, as Buttons and List items often use this pattern.

Here's an example of a custom ButtonView that gets its action as a closure:

import SwiftUI

struct ButtonView: View {
    let title: String
    let action: () -> Void

    init(title: String, action: @escaping () -> Void) {
        self.title = title
        self.action = action
    }

    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.title)
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
        }
    }
}

struct ContentView: View {
    var body: some View {
        ButtonView(title: "Press Me") {
            print("Button pressed!")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

In this example, ButtonView is a custom View that has a title and an action. This action is a closure that will be executed when the button is pressed. When we initialize an instance of ButtonView, we provide a title and action. This can be particularly useful when we want to reuse this ButtonView with different actions. In the ContentView, we are using ButtonView and passing a closure that prints "Button pressed!" when the button is tapped.

Another practical example of SwiftUI syntax

Let's assume we have an enum called AccountAction that represents various actions that can happen in a hypothetical account app (For example: deposit, withdraw). In this case, AccountAction could look something like this:

enum AccountAction {
    case deposit(amount: Double)
    case withdraw(amount: Double)
}

The AccountView struct below creates a SwiftUI view with two buttons: "Deposit" and "Withdraw". The actions for these buttons are defined by the action closure:

import SwiftUI

enum AccountAction {
    case deposit(amount: Double)
    case withdraw(amount: Double)
}

class AccountViewModel: ObservableObject {
    @Published private(set) var balance: Double = 0
    let actionClosure: (AccountAction) -> Void

    init(action: @escaping (AccountAction) -> Void) {
        self.actionClosure = action
    }

    func perform(action: AccountAction) {
        switch action {
        case .deposit(let amount):
            deposit(amount: amount)
        case .withdraw(let amount):
            withdraw(amount: amount)
        }
        self.actionClosure(action) // notify about the action
    }

    private func deposit(amount: Double) {
        balance += amount
    }

    private func withdraw(amount: Double) {
        balance -= amount
    }
}

struct AccountView: View {
    @ObservedObject var viewModel: AccountViewModel

    init(viewModel: AccountViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        VStack(spacing: 20) {
            Text("Balance: $\(viewModel.balance, specifier: "%.2f")")
            Button(action: {
                viewModel.perform(action: .deposit(amount: 100.0))
            }, label: {
                Text("Deposit $100")
            })

            Button(action: {
                viewModel.perform(action: .withdraw(amount: 50.0))
            }, label: {
                Text("Withdraw $50")
            })
        }
    }
}

struct AccountContentView: View {
    var body: some View {
        let viewModel = AccountViewModel { action in
            switch action {
            case .deposit(let amount):
                print("Deposited: \(amount)")
            case .withdraw(let amount):
                print("Withdrew: \(amount)")
            }
        }
        return AccountView(viewModel: viewModel)
    }
}

// MARK: - Preview
#Preview {
    AccountContentView()
}

This is a SwiftUI-based code for managing a simple banking operation. The program allows the user to deposit and withdraw money from an account, and it prints logs to reflect the actions performed.

Let's break down the code and see how it leverages closure as an argument to provide flexible functionality.

  1. AccountAction: This is an enum that consists of two possible actions that can be performed on the bank account - namely deposit or withdraw.

  2. AccountViewModel: This is the view model class that provides a bridge between the UI (AccountView) and the underlying logic for actually performing a deposit or withdrawal. The class is declared as ObservableObject, enabling the SwiftUI view to watch for changes.

  • @Published private(set) var balance: Double = 0: This line declares a balance property that keeps track of the current balance in the account. This is marked as @Published, so if balance changes, SwiftUI knows to update the UI. The private(set) access control means that balance can only be modified within the AccountViewModel, encapsulating the state better.

  • let actionClosure: ((AccountAction) -> Void): This is the declaration of a closure property. This closure gets an AccountAction as a parameter. When a deposit or withdrawal is made, this closure is called so the caller can know which action was performed.

  • init(action: @escaping (AccountAction) -> Void): This is the initializer for AccountViewModel. It takes a closure as an argument that is similar to actionClosure, and assigns it to actionClosure. The @escaping keyword is used because the closure will be stored to be called later, outside the initialization context.

  • perform(action: AccountAction): This method takes an AccountAction (either deposit or withdrawal), performs the operation and then calls actionClosure, passing the performed action.

  • deposit(amount: Double) and withdraw(amount: Double): These are the private methods for depositing and withdrawing an amount of money from the balance.

  1. AccountView: This SwiftUI View displays the current balance and has two buttons for depositing and withdrawing money.
  • init(viewModel: AccountViewModel): AccountView is initialized with an AccountViewModel, which it uses for performing actions and observing the balance.

  • VStack in the body: This contains a Text for displaying the account balance and two buttons for deposit and withdrawal actions. Each button calls the perform(action:) of the viewModel with the appropriate action.

  1. AccountContentView: This SwiftUI View creates an AccountViewModel with a closure that'll simply print when a deposit or withdrawal is made. It then initializes AccountView with that AccountViewModel.

In this code snippet, a closure is used as an argument while initializing AccountViewModel. This enables users of AccountViewModel to provide custom actions that'll be performed when a deposit or withdrawal is made, while keeping the control of when the action happens to the AccountViewModel itself. This is a very flexible pattern and is frequently used in SwiftUI.