Passing closure as an argument while initializing an object in Swift and SwiftUI
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.
AccountAction: This is an enum that consists of two possible actions that can be performed on the bank account - namely deposit or withdraw.
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 asObservableObject
, enabling the SwiftUI view to watch for changes.
@Published private(set) var balance: Double = 0
: This line declares abalance
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. Theprivate(set)
access control means thatbalance
can only be modified within theAccountViewModel
, encapsulating the state better.let actionClosure: ((AccountAction) -> Void)
: This is the declaration of a closure property. This closure gets anAccountAction
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 forAccountViewModel
. It takes a closure as an argument that is similar toactionClosure
, and assigns it toactionClosure
. 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 anAccountAction
(either deposit or withdrawal), performs the operation and then callsactionClosure
, passing the performed action.deposit(amount: Double)
andwithdraw(amount: Double)
: These are the private methods for depositing and withdrawing an amount of money from the balance.
- AccountView: This SwiftUI
View
displays the current balance and has two buttons for depositing and withdrawing money.
init(viewModel: AccountViewModel)
:AccountView
is initialized with anAccountViewModel
, which it uses for performing actions and observing the balance.VStack
in thebody
: This contains aText
for displaying the account balance and two buttons for deposit and withdrawal actions. Each button calls theperform(action:)
of theviewModel
with the appropriate action.
- AccountContentView: This SwiftUI
View
creates anAccountViewModel
with a closure that'll simply print when a deposit or withdrawal is made. It then initializesAccountView
with thatAccountViewModel
.
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.