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
107: Data Race Safety with Sendable Types
You've probably spent some time now working with async/await and Actors, and you might have encountered a warning that looks something like this: "Capture of 'x' with non-sendable type 'Y' in a function bounded to 'Sendable'". When this happens, a common instinct is to think: "I know this is safe because I'm not actually mutating the object inside the Task, so the compiler is just being over-cautious."
I'll be honest: that's a dangerous way to think. The compiler isn't being "over-cautious"; it's pointing out that you've created a hole in your safety net. Let me show you why your "I'm being careful" approach fails.
"I'm not mutating it, so it's safe" vs. The Reality of Shared Reference State
class UserSession {
var username: String
init(username: String) { self.username = username }
}
func updateUI(with session: UserSession) {
Task {
// I'm only reading the name! How can this be a data race?
print("Welcome back, \(session.username)")
}
}
In the code above, you aren't changing username inside the Task. But here is the problem: UserSession is a class. It's a reference type. Both the main thread (where updateUI is running) and the background thread (where the Task is executing) now hold a pointer to the exact same spot in memory.
While the Task is reading the username, some other piece of code—perhaps a network callback or a timer—could be writing to that same username property. That is a data race. It doesn't matter that the Task itself is "behaving"; the fact that the type is capable of being mutated from multiple places simultaneously makes it unsafe to move across concurrency boundaries.
Using Sendable to Prove Thread-Safety to the Compiler
To fix this, we use the Sendable protocol. Think of Sendable as a stamp of approval. It tells the Swift compiler, "I guarantee that this value can be safely passed from one thread to another without causing a data race."
For most of your data models, the easiest path is to use a struct. Because structs are value types, they are copied when passed around. If a Task gets its own copy of a struct, it doesn't matter what the original thread does with its version—they aren't sharing memory. Swift automatically makes structs Sendable if all their properties are also Sendable.
struct UserSession: Sendable {
let username: String // String is Sendable, so this works
}
Note that I changed var to let. While a struct with var properties is still technically Sendable (because the whole struct is copied), using constants makes your intent clear: this data is a snapshot, not a shared mutable object.
Handling Classes and the @unchecked Sendable Escape Hatch
You can't just slap Sendable onto a standard class because the compiler knows that classes are reference types. If you really need a class to be Sendable, you have a few choices. The "correct" modern choice is usually to turn that class into an actor, which protects its state through isolation.
However, you'll occasionally run into legacy code or low-level C-interop where you know a class is thread-safe (maybe it uses an internal DispatchQueue or a lock), but the compiler can't possibly prove it. In those rare cases, you use @unchecked Sendable.
class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: String] = [:]
func set(_ value: String, for key: String) {
lock.lock()
storage[key] = value
lock.unlock()
}
}
When you use @unchecked, you are telling Swift: "Stop checking this. I'm taking full responsibility for the synchronization." I strongly suggest you avoid this unless you have no other option. It's essentially turning off the safety features of the language for that specific type. If you mess up the locking logic, you're back to square one with hard-to-debug crashes.
📋 Practical Task
Exercise: Fixing the Concurrent ImageCache
You are working on a photo app. There is an ImageCache class that is causing concurrency warnings because it is being passed into several Task blocks to preload images. Currently, the cache is a standard class with a dictionary, which is not Sendable.
Your goal: Transform the ImageCache into a thread-safe Sendable type. While you could use @unchecked Sendable, the best practice here is to convert the class into an actor to ensure that the cache dictionary is never accessed by two threads at once.
// --- START OF BROKEN CODE ---
class ImageCache {
var cache: [String: String] = [:]
func save(imagePath: String, id: String) {
cache[id] = imagePath
}
func get(id: String) -> String? {
return cache[id]
}
}
func preloadImages(cache: ImageCache) {
Task {
// Warning: Capture of 'cache' with non-sendable type 'ImageCache'
let path = cache.get(id: "profile_pic")
print("Preloaded: \(path ?? "none")")
}
}
// --- END OF BROKEN CODE ---
Requirements:
- Convert
ImageCachefrom aclassto anactor. - Update the
preloadImagesfunction to handle the fact that accessing actor methods now requiresawait. - Ensure the code compiles without "non-sendable" warnings.
There are no comments for now.