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
147: Clean Architecture Layering in iOS Apps
By now, you've probably seen plenty of "MVVM" tutorials. But as your apps grow, you'll find that ViewModels often become "dumping grounds" for everything from API calls to complex business calculations. That's where Clean Architecture comes in. The goal isn't to add more folders for the sake of it; it's to ensure that your business logic doesn't care whether you're fetching data from a JSON API, a Firebase database, or a local plist file.
Let's build a simple "Current Weather" feature together. We're going to separate this into three distinct layers: Domain, Data, and Presentation.
Defining the Core Business Logic (Domain)
In Clean Architecture, we start from the inside out. The Domain layer is the heart of the app. It should be pure Swift—no UIKit, no SwiftUI, and definitely no networking libraries. If we decided to move this logic to a command-line tool tomorrow, the Domain layer shouldn't have to change.
// The Entity: A simple model that the rest of the app understands
struct Weather {
let temperature: Double
let condition: String
}
// The Repository Interface: We define WHAT we need, not HOW we get it
protocol WeatherRepository {
func fetchCurrentWeather(for city: String) async throws -> Weather
}
// The Use Case: A single, specific business action
class GetWeatherUseCase {
private let repository: WeatherRepository
init(repository: WeatherRepository) {
self.repository = repository
}
func execute(city: String) async throws -> Weather {
// This is where you'd put business rules (e.g., validation, caching logic)
return try await repository.fetchCurrentWeather(for: city)
}
}
Wiring up the Data Source (Data)
Now we move to the Data layer. This is where the "dirty" work happens. This layer implements the protocols defined in the Domain. It knows about URLs, JSON decoding, and API keys.
// A DTO (Data Transfer Object) that matches the API response
struct WeatherResponse: Decodable {
let temp: Double
let desc: String
}
class WeatherRepositoryImpl: WeatherRepository {
func fetchCurrentWeather(for city: String) async throws -> Weather {
let url = URL(string: "https://api.weather.com/v1/\(city)")!
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(WeatherResponse.self, from: data)
// Map the API model to the Domain entity
return Weather(temperature: response.temp, condition: response.desc)
}
}
Connecting to the UI via the ViewModel (Presentation)
Finally, the Presentation layer. The ViewModel doesn't talk to the Repository directly; it talks to the Use Case. This keeps the ViewModel slim and focused on formatting data for the view.
@MainActor
class WeatherViewModel: ObservableObject {
@Published var weatherDescription: String = ""
private let getWeatherUseCase: GetWeatherUseCase
init(getWeatherUseCase: GetWeatherUseCase) {
self.getWeatherUseCase = getWeatherUseCase
}
func updateWeather(for city: String) async {
do {
let weather = try await getWeatherUseCase.execute(city: city)
self.weatherDescription = "\(weather.temperature)°C and \(weather.condition)"
} catch {
self.weatherDescription = "Error loading weather"
}
}
}
Fixing a Leaky Abstraction
I'll be honest: when I first started implementing this pattern, I used to take shortcuts. I remember one project where I got lazy and passed the WeatherResponse (the API model) directly from the Repository all the way up to the ViewModel just to avoid writing the mapping code.
It seemed fine at first, but then the API provider changed the field temp to temperature_celsius. Because I leaked the Data model into the Presentation layer, I had to go into my ViewModel and my SwiftUI views to fix the naming. I had broken the fundamental rule of layering: The outer layers can depend on the inner layers, but the inner layers must never know about the outer layers.
If you find yourself importing your Networking module into your ViewModel, stop. Map that API response to a Domain entity inside your Repository. It feels like extra work now, but it saves you from a massive refactor when the backend team decides to change their JSON structure next month.
📋 Practical Task
Refactor the UserProfile Feature to Clean Architecture
You have a legacy UserProfileViewModel that currently handles networking directly using a UserAPIClient. The ViewModel is using a UserJSONResponse model which contains raw API fields like user_first_name and user_last_name.
Your task is to decouple this using the layering we discussed:
- Create a Domain Entity: Create a
UserProfilestruct with clean properties (e.g.,firstNameandlastName). - Define a Repository Protocol: Create a
UserRepositoryprotocol in the Domain layer that returns theUserProfileentity. - Implement the Data Layer: Create a
UserRepositoryImplthat calls theUserAPIClientand maps theUserJSONResponseinto theUserProfileentity. - Create a Use Case: Implement a
GetUserProfileUseCasethat coordinates the repository. - Update the ViewModel: Refactor the
UserProfileViewModelto depend on theGetUserProfileUseCaseinstead of the API client.
There are no comments for now.