Protocols Practice
Module Summary
Congratulations on completing the Protocols module! You've learned how to design flexible, composable APIs using Protocol-Oriented Programming.
What You've Learned
Define blueprints for types
Properties, methods, initializers
Compose protocol hierarchies
Generic protocols
Add functionality to types
Default implementations
Communication pattern
Equatable, Codable, etc.
Practice Exercises
1. Shape Drawing System
Combine Drawable, Colorable, Resizable protocols with default implementations.
2. API Service with Delegation
Implement delegate pattern for async API calls.
3. Codable Data Model
Create model with custom CodingKeys for JSON.
4. POP Game System
Compose game characters with Attackable, Defendable, Healable.
5. Generic Repository
Build reusable data storage with associated types.
Key Takeaways
- Start with protocols - Design APIs around protocols first
- Composition over inheritance - Use multiple protocols
- Default implementations - Share code via extensions
- Weak delegates - Avoid retain cycles
- Value types + protocols - POP shines with structs
// PROTOCOLS PRACTICE
// Comprehensive exercises covering all protocol concepts
// ===== EXERCISE 1: Shape Drawing System =====
protocol Drawable {
func draw()
}
protocol Colorable {
var color: String { get set }
}
protocol Resizable {
var scale: Double { get set }
mutating func resize(by factor: Double)
}
// Default implementation
extension Resizable {
mutating func resize(by factor: Double) {
scale *= factor
}
}
struct Rectangle: Drawable, Colorable, Resizable {
var width: Double
var height: Double
var color: String
var scale: Double = 1.0
func draw() {
let w = width * scale
let h = height * scale
print("Drawing \(color) rectangle: \(w)x\(h)")
}
}
var rect = Rectangle(width: 10, height: 5, color: "blue")
rect.draw()
rect.resize(by: 2.0)
rect.draw()
// ===== EXERCISE 2: API Service with Delegation =====
protocol APIServiceDelegate: AnyObject {
func apiDidStartLoading()
func apiDidFinishLoading(data: [String: Any])
func apiDidFailWithError(_ error: String)
}
class APIService {
weak var delegate: APIServiceDelegate?
func fetchData(endpoint: String) {
delegate?.apiDidStartLoading()
// Simulate API call
let success = true
if success {
let data: [String: Any] = ["users": ["Alice", "Bob"]]
delegate?.apiDidFinishLoading(data: data)
} else {
delegate?.apiDidFailWithError("Network error")
}
}
}
class DataManager: APIServiceDelegate {
let api = APIService()
init() {
api.delegate = self
}
func loadUsers() {
api.fetchData(endpoint: "/users")
}
func apiDidStartLoading() {
print("Loading...")
}
func apiDidFinishLoading(data: [String: Any]) {
print("Received: \(data)")
}
func apiDidFailWithError(_ error: String) {
print("Error: \(error)")
}
}
// ===== EXERCISE 3: Codable Data Model =====
struct User: Codable, Identifiable, Equatable {
let id: Int
var name: String
var email: String
var isActive: Bool
enum CodingKeys: String, CodingKey {
case id
case name
case email
case isActive = "is_active"
}
}
// Encode
let user = User(id: 1, name: "Alice", email: "[email protected]", isActive: true)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let json = try? encoder.encode(user) {
print(String(data: json, encoding: .utf8)!)
}
// ===== EXERCISE 4: Protocol-Oriented Game System =====
protocol Attackable {
var attackPower: Int { get }
func attack() -> Int
}
protocol Defendable {
var defense: Int { get }
func defend(damage: Int) -> Int
}
protocol Healable {
var health: Int { get set }
mutating func heal(amount: Int)
}
// Default implementations
extension Attackable {
func attack() -> Int {
return attackPower
}
}
extension Defendable {
func defend(damage: Int) -> Int {
return max(0, damage - defense)
}
}
extension Healable {
mutating func heal(amount: Int) {
health += amount
}
}
// Compose character types
struct Warrior: Attackable, Defendable, Healable {
var attackPower: Int = 20
var defense: Int = 15
var health: Int = 100
}
struct Mage: Attackable, Healable {
var attackPower: Int = 30
var health: Int = 60
}
struct Tank: Defendable, Healable {
var defense: Int = 30
var health: Int = 150
}
// ===== EXERCISE 5: Generic Repository =====
protocol Repository {
associatedtype Entity: Identifiable
func find(by id: Entity.ID) -> Entity?
func save(_ entity: Entity)
func delete(by id: Entity.ID)
func all() -> [Entity]
}
struct InMemoryRepository<T: Identifiable>: Repository where T.ID: Hashable {
private var storage: [T.ID: T] = [:]
func find(by id: T.ID) -> T? {
return storage[id]
}
mutating func save(_ entity: T) {
storage[entity.id] = entity
}
mutating func delete(by id: T.ID) {
storage.removeValue(forKey: id)
}
func all() -> [T] {
return Array(storage.values)
}
}
// Usage
struct Product: Identifiable {
let id: Int
var name: String
var price: Double
}
var productRepo = InMemoryRepository<Product>()
productRepo.save(Product(id: 1, name: "iPhone", price: 999))
productRepo.save(Product(id: 2, name: "iPad", price: 799))
print(productRepo.all())Module Complete!
You've mastered Swift Protocols! You now understand Protocol-Oriented Programming, the cornerstone of modern Swift development.
Next up: Error Handling - gracefully handle errors in your code!