Swift from Scratch: A Complete Guide (Part 4)

Protocol-Oriented Programming (POP)

Why POP Over OOP?

Protocol Extensions with Default Implementations

protocol Washable {
    func wash()
}

extension Washable {
    func wash() {
        print("Washing the object by default.")
    }
}

struct Car: Washable {}
struct Bike: Washable {
    func wash() {
        print("Washing the bike with special care.")
    }
}

// Usage
let car = Car()
car.wash()  // Output: Washing the object by default.

let bike = Bike()
bike.wash()  // Output: Washing the bike with special care.

Real-World Example: Equatable and Comparable Protocols

Using Equatable Protocol

struct Person: Equatable {
    let name: String
    let age: Int
}

let person1 = Person(name: "Alice", age: 25)
let person2 = Person(name: "Bob", age: 30)
let person3 = Person(name: "Alice", age: 25)

// Check Equality
print(person1 == person2)  // Output: false
print(person1 == person3)  // Output: true

Using Comparable Protocol

struct Product: Comparable {
    let name: String
    let price: Double

    static func < (lhs: Product, rhs: Product) -> Bool {
        return lhs.price < rhs.price
    }
}

let product1 = Product(name: "iPhone", price: 999.99)
let product2 = Product(name: "iPad", price: 799.99)

print(product1 < product2)  // Output: false

Reusable Protocol for Networking

let greet = { name in
return "Hello, \(name)!"
}

Reusable Protocol for Networking

Step 1

protocol NetworkRequest {
    func fetch(from url: String, completion: @escaping (Result<Data, Error>) -> Void)
}

extension NetworkRequest {
    func fetch(from url: String, completion: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: url) else {
            completion(.failure(NSError(domain: "Invalid URL", code: 400, userInfo: nil)))
            return
        }

        URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error {
                completion(.failure(error))
            } else {
                completion(.success(data ?? Data()))
            }
        }.resume()
    }
}

step 2

struct APIService: NetworkRequest {}

let service = APIService()
service.fetch(from: "https://jsonplaceholder.typicode.com/posts") { result in
    switch result {
    case .success(let data):
        print("Data fetched successfully: \(data)")
    case .failure(let error):
        print("Error occurred: \(error.localizedDescription)")
    }
}

Property Wrapper

Key Benefits of Property Wrappers

Basic Syntax of Property Wrappers

Step 1: Define a Property Wrapper

@propertyWrapper
struct Capitalized {
    private var value: String = ""
    
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}

Step 2: Use the Property Wrapper

struct User {
    @Capitalized var name: String
}

var user = User()
user.name = "john doe"
print(user.name)  // Output: John Doe

Built-in Property Wrappers in Swift

@State in SwiftUI

import SwiftUI

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

@Published in Combine

import Combine

class Counter: ObservableObject {
    @Published var count = 0
}

let counter = Counter()
let subscription = counter.objectWillChange.sink {
    print("Count will change to: \(counter.count)")
}

counter.count = 1  // Prints: Count will change to: 1

Conclusion

Codable (Serialization and Deserialization)

How to Use Codable?

Basic JSON Parsing

{
  "id": 101,
  "name": "John Doe",
  "email": "john@example.com"
}

Step 1: Create a Swift Struct

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

Step 2: Decode JSON into Swift Object

let jsonString = """
{
    "id": 101,
    "name": "John Doe",
    "email": "john@example.com"
}
"""

if let jsonData = jsonString.data(using: .utf8) {
    do {
        let user = try JSONDecoder().decode(User.self, from: jsonData)
        print(user.name) // Output: John Doe
    } catch {
        print("Failed to decode JSON: \(error)")
    }
}

Step 3: Encode Swift Object to JSON

let user = User(id: 101, name: "John Doe", email: "john@example.com")

do {
    let jsonData = try JSONEncoder().encode(user)
    if let jsonString = String(data: jsonData, encoding: .utf8) {
        print(jsonString)
        // Output: {"id":101,"name":"John Doe","email":"john@example.com"}
    }
} catch {
    print("Failed to encode JSON: \(error)")
}

Customizing Serialization Using CodingKeys

Example: JSON with Different Key Names

{
  "user_id": 101,
  "full_name": "John Doe",
  "user_email": "john@example.com"
}

Swift Struct

struct User: Codable {
    let id: Int
    let name: String
    let email: String

    enum CodingKeys: String, CodingKey {
        case id = "user_id"
        case name = "full_name"
        case email = "user_email"
    }
}

The process of decoding and encoding will be the same, only the coding case defines the mapping between objects and Swift properties.

Error Handling with Codable

do {
    let user = try JSONDecoder().decode(User.self, from: jsonData)
    print(user)
} catch DecodingError.keyNotFound(let key, let context) {
    print("Missing key: \(key.stringValue) - \(context.debugDescription)")
} catch DecodingError.typeMismatch(let type, let context) {
    print("Type mismatch: \(type) - \(context.debugDescription)")
} catch {
    print("Decoding failed: \(error.localizedDescription)")
}

Conclusion

Concurrency with async/await

Older Techniques: Closures and DispatchQueue

DispatchQueue.global().async {
    // Background work
    let result = "Hello, World"
    
    DispatchQueue.main.async {
        // Update UI on main thread
        print(result)
    }
}

Modern async/await (Swift 5.5 and later)

func fetchData() async -> String {
    // Simulating background work
    return "Hello, World"
}

Task {
    let result = await fetchData()
    print(result)  // Output: Hello, World
}

Writing Concurrent Code with Task and TaskGroup

Using Task for Simple Concurrency

func fetchData1() async -> String {
    return "Data 1"
}

func fetchData2() async -> String {
    return "Data 2"
}

Task {
    let data1 = await fetchData1()
    let data2 = await fetchData2()
    print(data1)
    print(data2)
}

Using TaskGroup for Concurrent Tasks with Results

func fetchData1() async -> String {
    return "Data 1"
}

func fetchData2() async -> String {
    return "Data 2"
}

Task {
    await withTaskGroup(of: String.self) { group in
        group.addTask {
            return await fetchData1()
        }
        
        group.addTask {
            return await fetchData2()
        }
        
        for await result in group {
            print(result)
        }
    }
}

Conclusion

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *