Leveraging Swift Package Manager to Maintain Clean Architecture Layers in iOS Development — Part 3
PresentationLayer Implementation
In the previous parts of this series, we discussed the structure and responsibilities of the Domain.
Now, it’s time to explore the Presentation Layer, the layer closest to the user. This is where we integrate our business logic (use cases) with user interfaces (UI), adhering to the separation of concerns principle. We’ll also demonstrate how SwiftUI allows us to write declarative, reactive views that pair seamlessly with the ObservableObject pattern.
Designing the Presentation Layer
The Presentation Layer’s responsibility is to handle data transformation and state management for the UI. In our modularized clean architecture, this layer depends only on the Domain layer to fetch data via use cases. By keeping the Presentation Layer decoupled from the Data Layer, we make it easier to maintain, test, and evolve.
ViewModel: Bridging the Domain Layer and Views
The CharacterListViewModel
serves as the binding between the CharacterListUsecase
(from the Domain Layer) and the SwiftUI view. It adheres to the ObservableObject protocol, enabling seamless state updates in the UI.
@MainActor
public final class CharacterListViewModel: ObservableObject {
@Published private(set) var characters: [CharacterViewModel] = []
private let characterListUseCase: CharacterListUsecase
public init(characterListUseCase: CharacterListUsecase) {
self.characterListUseCase = characterListUseCase
}
func fetchCharacters() {
let usecase = characterListUseCase
Task {
do {
let fetchedCharacters = try await usecase.execute()
self.characters = fetchedCharacters.results.map {
CharacterViewModel(id: $0.id, name: $0.name, species: $0.species, image: $0.image)
}
} catch {
print(error.localizedDescription)
}
}
}
}
SwiftUI View: Declarative UI with State Binding
The CharacterListView
is a SwiftUI view responsible for rendering a grid of character cards. It uses the CharacterListViewModel
as its state object.
public struct CharacterListView: View {
@StateObject var viewModel: CharacterListViewModel
public init(viewModel: CharacterListViewModel) {
_viewModel = .init(wrappedValue: viewModel)
}
public var body: some View {
ScrollView {
LazyVGrid(columns: [.init(.flexible()), .init(.flexible()), .init(.flexible())]) {
ForEach(viewModel.characters, id: \.id) { character in
CharacterView(viewModel: character)
}
}
.padding()
}
.onAppear {
viewModel.fetchCharacters()
}
}
}
Character ViewModel: Handling Asynchronous Image Loading
For each character card, we use a lightweight CharacterViewModel
to manage the individual character's properties and load their images asynchronously.
@MainActor
final class CharacterViewModel: ObservableObject {
@Published private(set) var id: Int
@Published private(set) var name: String
@Published private(set) var species: String
@Published private(set) var uiImage: UIImage?
public init(id: Int, name: String, species: String, image: String) {
self.id = id
self.name = name
self.species = species
Task {
do {
let (data, _) = try await URLSession.shared.data(from: URL(string: image)!)
uiImage = UIImage(data: data)
} catch {
print(error.localizedDescription)
}
}
}
}
Character View: Reusable Component for Each Card
The CharacterView
renders individual character data:
struct CharacterView: View {
@ObservedObject var viewModel: CharacterViewModel
var body: some View {
LazyVStack {
if let image = viewModel.uiImage {
Image(uiImage: image)
.resizable()
.frame(height: 120)
.background(Color.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Text(viewModel.name)
.font(.headline)
.lineLimit(1)
Text(viewModel.species)
.font(.subheadline)
.lineLimit(1)
}
}
}
PresentationLayer
│
├── Components
│ ├── CharacterView
│ ├── CharacterViewModel
│
├── Pages
│ ├── CharacterListView
│ ├── CharacterListViewModel