Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
33: Actors and Data Race Safety
Why can't I just use a class with a lock or a DispatchQueue?
You totally can. For years, that's exactly how we did it. But here's the problem: the compiler doesn't know you're using a lock. If you or a teammate forget to wrap a single property access in that lock, you've got a data race, and your app will crash randomly in production with a "heap corruption" error that is a nightmare to debug.
Actors change the game because they move the responsibility from your memory to the compiler. When you declare something as an actor, Swift guarantees that only one task can access its mutable state at a time. It's like a class that comes with its own built-in, invisible serial queue. If you try to touch an actor's properties from the outside without the proper synchronization, the code simply won't compile. I'd much rather have the compiler yell at me during development than have a customer report a crash I can't reproduce.
actor BankAccount {
var balance: Decimal = 0
func deposit(amount: Decimal) {
balance += amount
}
}
Why do I have to use await just to read a variable?
This is usually the first thing that trips people up. If BankAccount is an actor, you might expect to just call account.balance. But because the actor protects its state, any access from the "outside" is potentially dangerous. The actor might be busy processing a huge transaction on another thread.
When you use await, you're essentially saying, "I know this actor might be busy, so I'll pause my execution here and let the actor finish its current work before I step inside." It's a non-blocking pause. Your thread isn't frozen; it's just freed up to do other things until the actor is ready for you.
func printBalance(account: BankAccount) async {
// We must await because we are crossing the "actor boundary"
let currentBalance = await account.balance
print("Your balance is \(currentBalance)")
}
What if I have a property that doesn't actually need protection?
Sometimes you'll have a property inside an actor—like a constant ID or a name—that never changes. Forcing every single piece of code to await just to read a static string is annoying and adds unnecessary overhead. This is where nonisolated comes in.
By marking a property or method as nonisolated, you're telling Swift, "I promise this specific part of the actor is thread-safe on its own." This allows you to access that property synchronously, without await. Just be careful: you cannot touch any other mutable state inside the actor from a nonisolated context, or the compiler will stop you immediately.
actor BankAccount {
let accountID: String // Constants are implicitly nonisolated
var balance: Decimal = 0
nonisolated func getAccountType() -> String {
return "Checking Account" // This is safe because it doesn't touch 'balance'
}
}
// Now we can do this without 'await'
let type = myAccount.getAccountType()
Does this mean actors solve all my concurrency problems?
Not quite. Actors solve data races (multiple threads hitting the same memory), but they don't solve logic races.
Imagine you check a balance, see it's enough for a purchase, and then call a withdraw function. Between the time you checked the balance (first await) and the time you withdrew the money (second await), another task could have slipped in and emptied the account. This is called "actor reentrancy." To fix this, you should wrap the entire check-and-withdraw logic into a single actor method so it happens as one atomic operation. Don't break your logic into multiple await calls if they depend on the state remaining unchanged.
📋 Practical Task
Exercise: Fixing the Race Condition in the Digital Wallet
You've been handed a Wallet class that is causing crashes during stress tests because multiple threads are updating the funds and transactionLog simultaneously. Your task is to transform this class into a thread-safe actor.
// TODO: Convert this class to an actor
class Wallet {
var funds: Double = 0
var transactionLog: [String] = []
func addMoney(amount: Double) {
funds += amount
transactionLog.append("Added \(amount)")
}
func spendMoney(amount: Double) {
if funds >= amount {
funds -= amount
transactionLog.append("Spent \(amount)")
}
}
}
// Your testing code should look something like this:
// Task {
// let myWallet = Wallet()
// await myWallet.addMoney(amount: 100)
// ...
// }
Requirements:
- Change
Walletfrom aclassto anactor. - Ensure the
fundsandtransactionLogremain private or protected by the actor's isolation. - Implement a
nonisolatedproperty calledwalletVersionthat returns a hardcoded string "1.0" (simulating a constant that doesn't need synchronization). - Update the calling code to use
awaitwhere necessary to satisfy the compiler.
There are no comments for now.