Skip to content

Latest commit

 

History

History
197 lines (174 loc) · 7.73 KB

File metadata and controls

197 lines (174 loc) · 7.73 KB

System Architecture — Pok-API

Clean Architecture with MVVM + Combine reactive pipeline.

Layered Architecture (ASCII Diagram)

┌─────────────────────────────────────────────────────────┐
│              PRESENTATION LAYER (UIKit)                 │
│  ViewController → ViewModel (@Published) → UIView       │
│  PokemonListViewController, PokemonDetailViewController │
└──────────────┬──────────────────────────────────────────┘
               │ (calls UseCases)
┌──────────────▼──────────────────────────────────────────┐
│              DOMAIN LAYER (Pure Swift)                  │
│  UseCases ← Entities + Repository Protocols             │
│  FetchPokemonListUseCase, PokemonRepositoryProtocol    │
└──────────────┬──────────────────────────────────────────┘
               │ (depends on)
┌──────────────▼──────────────────────────────────────────┐
│              DATA LAYER (Implementation)                │
│  Repositories (DTO → Entity mapping)                    │
│  Network: APIClient + APIEndpoint                       │
│  Local: FavoritesRepository (UserDefaults)              │
└──────────────┬──────────────────────────────────────────┘
               │ (makes requests to)
┌──────────────▼──────────────────────────────────────────┐
│            EXTERNAL (PokeAPI v2 REST)                   │
│  https://pokeapi.co/api/v2/pokemon, /pokemon/{id}, etc. │
└─────────────────────────────────────────────────────────┘

Data Flow: List Screen Example

User taps PokemonListViewController
        ↓
    viewDidLoad()
        ↓
    viewModel.loadPokemon()
        ↓
    FetchPokemonListUseCase.execute(offset, limit)
        ↓
    PokemonRepository.fetchPokemonList()
        ↓
    APIClient.request<PokemonListResponseDTO>(.pokemonList)
        ↓
    URLSession.dataTaskPublisher() → tryMap() → decode() → mapError()
        ↓
    PokemonListResponseDTO received
        ↓
    Map: [PokemonListResponseDTO.result] → [Pokemon] (add imageURL)
        ↓
    AnyPublisher<[Pokemon], Error> flows back through chain
        ↓
    ViewModel.sink() → receive(on: .main)
        ↓
    @Published var pokemon: [Pokemon] updated
        ↓
    ViewController observes @Published (sink listener)
        ↓
    collectionView.reloadData() ← PokemonCell displays

Data Transformation Pipeline (API to Display)

PokeAPI JSON (/pokemon?offset=0&limit=20)
    ↓
URLSession.dataTaskPublisher() decodes JSON
    ↓
PokemonListResponseDTO (DTO layer)
    │
    ├─ results[].id
    ├─ results[].name
    └─ results[].url (ignored, id extracted)
    ↓
Repository maps DTO → Domain Entity
    │
    ├─ id: Int
    ├─ name: String (capitalized)
    ├─ imageURL: URL? (constructed from id)
    └─ types: [PokemonType] (empty for list, filled in detail)
    ↓
[Pokemon] entity array
    ↓
ViewModel @Published property
    ↓
UICollectionView DataSource reads pokemon[]
    ↓
PokemonCell renders: image + name + ID + type badges

Combine Patterns Used

1. Request Pipeline (APIClient)

session.dataTaskPublisher(for: url)
    .tryMap { data, response in /* validate */ }
    .decode(type: T.self, decoder: decoder)
    .mapError { /* convert to APIError */ }
    .eraseToAnyPublisher()

Key: Error handling at each stage, strong typing via decode().

2. ViewModel Subscription (weak self)

fetchListUseCase.execute(offset, limit)
    .receive(on: DispatchQueue.main)
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.isLoading = false
        },
        receiveValue: { [weak self] newPokemon in
            self?.pokemon.append(contentsOf: newPokemon)
        }
    )
    .store(in: &cancellables)

Key: Always [weak self], always .receive(on: .main), always .store().

3. Filter Pipeline (CombineLatest3)

Publishers.CombineLatest3($searchText, $selectedType, $pokemon)
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .map { searchText, type, allPokemon in
        /* apply filters */
    }
    .assign(to: &$filteredPokemon)

Key: Combine 3 inputs, debounce user typing, auto-assign to @Published property.

4. Parallel Loading (zip)

detailPublisher  // AnyPublisher<PokemonDetailDTO, APIError>
    .zip(speciesPublisher)  // AnyPublisher<PokemonSpeciesDTO, APIError>
    .map { detail, species in
        /* merge into single entity */
    }

Key: Wait for both requests, map combined result.

Dependency Injection

DIContainer (Singleton Pattern)

DIContainer.shared
├── lazy var apiClient: APIClient
├── lazy var pokemonRepository: PokemonRepositoryProtocol
├── lazy var favoritesRepository: FavoritesRepositoryProtocol
└── factory methods:
    ├── makeFetchPokemonListUseCase()
    ├── makeFetchPokemonDetailUseCase()
    ├── makeFetchEvolutionChainUseCase()
    └── makeSearchPokemonUseCase()

ViewController Injection

let vm = PokemonListViewModel(
    fetchListUseCase: container.makeFetchPokemonListUseCase(),
    favoritesRepository: container.favoritesRepository
)
let vc = PokemonListViewController(viewModel: vm, container: container)

Memory Management

  • Singletons: APIClient, Repositories held by DIContainer (lifetime = app)
  • ViewControllers: ViewModel captured weakly in closures ([weak self])
  • Subscriptions: .store(in: &cancellables) on BaseViewController property
  • Cancellation: Auto-deinit when ViewController pops (cancellables released)

Separation of Concerns

Layer Responsibility Never Includes
Domain Business logic, entity definitions, use case orchestration UIKit, Network, Persistence
Data API calls, DTO decoding, Repository impl, local storage UI logic, business rules
Presentation ViewController lifecycle, state binding, user interaction Network calls, database queries

Key Architectural Decisions

  1. Combine over RxSwift: Native, smaller surface area, good for MVVM
  2. Constructor DI over Service Locator: Explicit dependencies, testable
  3. Weak self pattern: Prevents cycles (VC → VM → Use Case → VM cycle)
  4. Debounce search: .milliseconds(300) for responsive filtering
  5. Pagination: offset/limit loop, 20 per page, canLoadMore flag
  6. Favorites persistence: UserDefaults (simple, no need for CoreData)
  7. Error handling: Typed APIError enum, user-facing strings

Testing Strategy (Future)

  • Domain layer: Unit tests for UseCases (mock repositories)
  • Data layer: Unit tests for Repositories (mock APIClient)
  • Presentation layer: ViewModel unit tests (mock use cases), ViewController UI tests (mock VMs)
  • Integration: Network mock tests (validate DTO → Entity mapping)