Question 4
Repositories, use cases, and data boundaries
View models are calling URLSession directly and caching in static vars. How would you introduce a repository or use-case layer without over-engineering?
Follow-ups
- How do you handle offline vs online in one place?
Answer outline
In plain terms, a repository is a single type (or module) your feature code asks for data. e.g. 'give me this user’s profile', without caring whether the answer comes from the network or a local persistent source.
The view model calls something like userRepository.profile(id:) instead of scattering URLSession, JSONDecoder, and file I/O across every screen.
It's a abstraction behind which you centralize logic such as: when to hit the network, when to reuse a cached value, how long a cache is valid (TTL) etc...
Principles
- A repository is not 'the database' or 'the API' by itself, it coordinates them for one area of data (users, checkout, etc.).
- Keep
Codablenetwork response structs near the network layer; map to domain types before the view model when JSON and app models diverge. - Prefer
actorinside the repo if mutable cache is shared across tasks. - Start with one repo per aggregate (e.g.
FeedRepository), not aGodRepositorythat knows everything.
final class ProfileViewModel {
private let users: UserRepository
init(users: UserRepository) { self.users = users }
func load() async throws {
let user = try await users.user(id: currentId)
// format for UI — no URLSession here
}
}
protocol UserRepository {
func user(id: String) async throws -> User
}
final class LiveUserRepository: UserRepository {
func user(id: String) async throws -> User {
// URLSession → decode response struct → map to User
}
}
Follow-up angles
- Offline-first: repository returns local immediately and refreshes in background.
- Core Data / SwiftData stack usually lives below repositories, not inside SwiftUI views.