Digital Auxilio Technologies Blog
Concurrency Problem

Concurrency in Swift with Async & Await

Hello All Swift enthusiasts.
Today, let us dive into the amazing world of concurrency in Swift and look at how the use of async/await has made our job easy as an iOS Developers.

The Concurrency Problem

Concurrency is like juggling multiple tasks at once – it’s all about managing and executing tasks simultaneously. In the realm of Swift, handling concurrency traditionally involved closures, but let’s be real, closures could be a bit unwieldy. Enter async/await – the new sheriff in town, making concurrent programming in Swift more readable and enjoyable.

The Saviour : Closures

Before we get our hands dirty with async/await, let’s take some time to appreciate closures. They served us well in handling asynchronous tasks, but they had their quirks. Nested closures could resemble a labyrinth, making code readability an uphill battle. Here’s a quick peek at how closures used to handle asynchronous operations:

func transferMoney(completion: @escaping (Bool) -> Void) {
    // Async task
    DispatchQueue.global().async {
        // Money transfer logic
        let success = performMoneyTransfer()
        // Callback
        DispatchQueue.main.async {
            completion(success)
        }
    }
}

It gets the job done, but it’s not the most elegant solution. Enter async/await – the syntax superhero that swoops in to save the day!

Enter the Hero: Async/Await

Async/await is a game-changer, simplifying asynchronous code and making it look like synchronous code. Let’s rewrite our money transfer function using this new duo:

func transferMoney() async -> Bool {
    // Money transfer logic
    let success = await performMoneyTransfer()
    return success
}

Clean, concise, and easy to follow – that’s the beauty of async/await. But hold your horses; let’s not get too carried away. It’s essential to recognize that async/await isn’t a silver bullet; it has its own set of challenges.

The Dark Side: Drawbacks of Async/Await

  1. Learning Curve: As with any new concept, there’s a learning curve. Developers need to familiarize themselves with the async/await syntax and understand its intricacies.
  2. Tooling Support: While async/await is now a part of Swift, full tooling support might still be catching up. Debugging asynchronous code could be a bit trickier than synchronous code.
  3. Compatibility Issues: If you’re working on a project that supports older Swift versions, you might run into compatibility issues. Async/await is available starting from Swift 5.5.

Now, let’s put our newfound knowledge to the test with a real-world example – a banking system with user transactions.

Example: Banking System with Async/Await

struct User {
    var balance: Double = 1000.0
}

func performMoneyTransfer(amount: Double, from sender: inout User, to receiver: inout User) async -> Bool {
    // Async task simulating money transfer
    await Task.sleep(1 * 1_000_000_000) // Simulate some processing time
    
    if sender.balance >= amount {
        sender.balance -= amount
        receiver.balance += amount
        return true
    } else {
        return false
    }
}

// Example usage
async {
    var user1 = User()
    var user2 = User()

    let success = await performMoneyTransfer(amount: 500.0, from: &user1, to: &user2)

    if success {
        print("Money transfer successful!")
        print("User 1 balance: \(user1.balance), User 2 balance: \(user2.balance)")
    } else {
        print("Insufficient funds for money transfer.")
    }
}

There you have it – a simple banking system using async/await in Swift. We’ve come a long way from nested closures to this elegant and readable async/await syntax.

Detailed Example

import Foundation

// MARK: - Enums

enum TransactionType {
    case deposit
    case withdrawal
    case transfer
}

// MARK: - Property Wrapper

@propertyWrapper
struct ValidAmount {
    private var value: Double

    var wrappedValue: Double {
        get { value }
        set {
            if newValue >= 0 {
                value = newValue
            } else {
                print("Invalid amount. Amount should be non-negative.")
            }
        }
    }

    init(initialValue: Double) {
        self.value = initialValue
    }
}

// MARK: - Model

class BankAccount {
    var accountHolder: String
    @ValidAmount(initialValue: 0.0) var balance: Double

    init(accountHolder: String) {
        self.accountHolder = accountHolder
    }

    func performTransaction(type: TransactionType, amount: Double) async throws {
        switch type {
        case .deposit:
            balance += amount
            print("Deposit of \(amount) successful. New balance: \(balance)")

        case .withdrawal:
            guard balance >= amount else {
                throw BankError.insufficientFunds
            }
            balance -= amount
            print("Withdrawal of \(amount) successful. New balance: \(balance)")

        case .transfer:
            throw BankError.invalidTransaction
        }
    }
}

class SavingsAccount: BankAccount {
    var interestRate: Double

    init(accountHolder: String, interestRate: Double) {
        self.interestRate = interestRate
        super.init(accountHolder: accountHolder)
    }

    override func performTransaction(type: TransactionType, amount: Double) async throws {
        try await super.performTransaction(type: type, amount: amount)

        switch type {
        case .deposit:
            applyInterest()
        default:
            break
        }
    }

    private func applyInterest() {
        let interest = balance * interestRate
        balance += interest
        print("Interest applied. New balance: \(balance)")
    }
}

// MARK: - Error

enum BankError: Error {
    case insufficientFunds
    case invalidTransaction
}

// MARK: - Example Usage

async {
    do {
        var userAccount = BankAccount(accountHolder: "John Doe")
        try await userAccount.performTransaction(type: .deposit, amount: 1000.0)
        try await userAccount.performTransaction(type: .withdrawal, amount: 500.0)

        var savingsAccount = SavingsAccount(accountHolder: "Jane Doe", interestRate: 0.05)
        try await savingsAccount.performTransaction(type: .deposit, amount: 2000.0)
        try await savingsAccount.performTransaction(type: .withdrawal, amount: 300.0)
    } catch {
        print("Error: \(error)")
    }
}
  • We have two types of transactions: TransactionType.deposit, TransactionType.withdrawal, and TransactionType.transfer.
  • The @ValidAmount property wrapper ensures that the amount is non-negative.
  • The BankAccount class handles basic banking operations like deposits and withdrawals, and the SavingsAccount class inherits from it, adding the ability to apply interest on deposits.
  • Asynchronous tasks are performed using the async and await keywords.
  • Error handling is done through Swift’s throws and Error protocol, with specific error cases defined in the BankError enum.

In conclusion


Async and await is a powerful tool in Swift’s arsenal, providing a cleaner and more readable way to handle asynchronous tasks. While it’s not without its challenges, the benefits it brings to the table make it a valuable addition to the Swift developer’s toolkit. So, go ahead, embrace the concurrency magic, and let your code shine!

ForamVyas

CTO @ Book Donate | Project Manager @ DigitalAuxilio | Co-Founder @ WeCare Consumer Products

๐Ÿ“Œ About Me:
With a decade of dedicated experience in the dynamic world of IT, I can be your trustworthy professional for delivering cutting-edge software solutions, effective project management, and unparalleled client satisfaction. My passion for technology and love for fostering strong client relationships have been the cornerstones of my success.

Add comment

Follow us

Don't be shy, get in touch. We love meeting interesting people and making new friends.

Most popular

Most discussed