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
45: Associated Types in Protocols
You've already spent time with generics in structs and classes, so the concept of "a type that we'll decide later" isn't new to you. But when you move that logic into a protocol, Swift handles it differently. You can't just put <T> next to a protocol name. Instead, we use associatedtype.
Let's build a simple caching system for a music app. We want a way to store different kinds of data—like Song objects or Artist objects—without writing a completely separate protocol for every single model in our app.
The mistake of using Any
When I first approached this, I tried to keep it "simple" by using Any. It looked like this:
protocol Cache {
func save(_ item: Any)
func retrieve() -> Any?
}
On the surface, it works. You can pass anything in. But as soon as I tried to actually use the retrieved item, I had to cast it manually every single time: if let song = cache.retrieve() as? Song { ... }. That's a nightmare. It's fragile, it's verbose, and it completely defeats the purpose of Swift's strong type system. I was basically turning Swift into a dynamically typed language, and that's a path you don't want to go down.
Defining the placeholder with associatedtype
The right way to do this is to tell the protocol: "I don't know exactly what type will be stored here, but whatever the conforming struct or class decides, that's the type we'll use consistently."
protocol Cache {
associatedtype Item
func save(_ item: Item)
func retrieve() -> Item?
}
Notice I didn't specify if Item is a String, an Int, or a custom Object. I just gave it a name. Now, the protocol is a blueprint that says: "Whoever implements me must define what Item is."
Locking in the types with concrete implementations
Now we can create specific caches. Let's say we have a Song struct. When we create a SongCache, Swift will infer the associatedtype based on the function signatures we provide.
struct Song {
let title: String
let artist: String
}
struct SongCache: Cache {
// Swift sees 'Song' here and realizes 'Item' must be 'Song'
var storage: [String: Song] = [:]
func save(_ item: Song) {
storage[item.title] = item
}
func retrieve() -> Song? {
// Simplified for the example
return storage.values.first
}
}
I love this because it's clean. If I try to pass an Artist object into SongCache.save(), the compiler will stop me immediately. We get the flexibility of a generic protocol but the safety of concrete types.
The "Protocol with Associated Type" (PAT) quirk
Here is where you might hit a wall. You might try to create an array of different caches like this: let myCaches: [Cache] = [songCache, artistCache]. This will fail.
Swift will complain that Cache has an associated type and cannot be used as a type itself. This is because the compiler needs to know exactly what Item is to ensure memory safety. You can't have a list of "things that are caches" because one might be a Song cache and another an Artist cache, and the compiler can't guarantee what retrieve() will return.
To get around this, you'll usually use generics in the functions that consume these caches, like this:
func clearCache<T: Cache>(cache: T) {
print("Clearing the cache for \(T.Item.self)")
}
By using <T: Cache>, we're telling Swift: "I don't care which specific cache this is, as long as it follows the Cache protocol."
📋 Practical Task
Build a Type-Safe Configuration Manager
You need to create a system that provides configuration settings for different parts of an app. Some settings are simple strings (like API keys), while others are complex objects (like a DatabaseConfig struct).
Your Requirements:
- Create a protocol named
ConfigurationProvider. It should have anassociatedtypecalledConfigValue. - The protocol must require a method
fetchSetting(for key: String) -> ConfigValue?. - Create a struct called
DatabaseConfigwith properties forurlandtimeout. - Implement a
NetworkConfigProviderthat conforms toConfigurationProviderand handlesStringvalues. - Implement a
DatabaseConfigProviderthat conforms toConfigurationProviderand handlesDatabaseConfigvalues. - Write a generic function
printSetting<P: ConfigurationProvider>(provider: P, key: String)that fetches the setting and prints it to the console.
There are no comments for now.