Skip to content

Latest commit

 

History

History
370 lines (308 loc) · 9.19 KB

File metadata and controls

370 lines (308 loc) · 9.19 KB

Code Standards — Pok-API

Naming conventions, architectural rules, and Combine patterns for consistency.

Naming Conventions

Files (PascalCase)

  • ViewController: PokemonListViewController.swift
  • ViewModel: PokemonListViewModel.swift
  • View/Cell: PokemonCell.swift, DetailHeaderView.swift
  • UseCase: FetchPokemonListUseCase.swift
  • Repository: PokemonRepository.swift
  • Entity: Pokemon.swift
  • DTO: PokemonDetailDTO.swift
  • Protocol: PokemonRepositoryProtocol.swift
  • Enum: APIEndpoint.swift, APIError.swift, PokemonType.swift

Types (PascalCase)

class PokemonListViewController { }
struct Pokemon { }
enum PokemonType { case fire, water }
protocol PokemonRepositoryProtocol { }

Variables & Properties (camelCase)

var pokemonList: [Pokemon] = []
let imageURL: URL?
var isLoading = false
let currentOffset = 0

Functions & Methods (camelCase)

func loadPokemon() { }
func fetchPokemonDetail(id: Int) -> AnyPublisher { }
func isFavorite(id: Int) -> Bool { }

Constants (camelCase, not SCREAMING)

private let pageSize = 20
private let debounceMs: UInt64 = 300
private let key = "pokemon_favorites"

Private/Internal (Prefix underscore optional, prefer explicit fileprivate/private)

fileprivate var cancellables = Set<AnyCancellable>()
private let repository: PokemonRepositoryProtocol
private func mapToDetail(detail: PokemonDetailDTO) { }

Architectural Rules

Domain Layer (Pure Swift)

  • Must not import: UIKit, Foundation.URLSession
  • Can import: Foundation (for basic types, Codable, URL)
  • Entities: Structs, immutable, no logic
  • UseCases: Final classes, single responsibility, return AnyPublisher
  • Protocols: Define contracts only, no default impl

Data Layer (DTO Mapping)

  • All DTOs live here, never used outside this layer
  • Repositories: Final classes, implement Domain protocols
  • APIClient: Single responsibility — HTTP + JSON decoding only
  • APIEndpoint: Pure enum, no side effects
  • Mapping: DTO → Entity happens in Repository methods
  • Network errors: Map to typed APIError enum

Example (CORRECT):

// Data/Repositories/PokemonRepository.swift
func fetchPokemonList() -> AnyPublisher<[Pokemon], Error> {
    apiClient.request(endpoint: .pokemonList)
        .map { (dto: PokemonListResponseDTO) in
            // DTO lives here, hidden from Domain
            dto.results.map { Pokemon(...) }
        }
        .eraseToAnyPublisher()
}

Example (WRONG):

// Domain/UseCases/FetchPokemonListUseCase.swift
// NEVER import or reference DTO
import PokeAPI // Don't expose Data layer
func execute() -> AnyPublisher<[PokemonDTO], Error> { } // WRONG

Presentation Layer (UIKit)

  • ViewController: Init with dependencies, setup in viewDidLoad()
  • ViewModel: No UIKit imports, pure Swift + Combine
  • Views: Init with default parameters, layout in layoutSubviews() or init(frame:)
  • Cells: Reuse ID constant, register in ViewController
  • BaseViewController: Provides cancellables Set, nothing else

Combine Patterns (MANDATORY)

1. Memory Safety: Always [weak self]

// CORRECT
fetchUseCase.execute()
    .sink { [weak self] value in
        self?.vm.state = value
    }
    .store(in: &cancellables)

// WRONG
fetchUseCase.execute()
    .sink { value in  // Captures self strongly
        self.vm.state = value
    }
    .store(in: &cancellables)

2. Main Thread: Always .receive(on: DispatchQueue.main)

// CORRECT
fetchUseCase.execute()
    .receive(on: DispatchQueue.main)  // Before sink
    .sink { [weak self] in
        self?.updateUI()
    }
    .store(in: &cancellables)

// WRONG
fetchUseCase.execute()
    .sink { [weak self] in
        DispatchQueue.main.async {  // Too late, causes race
            self?.updateUI()
        }
    }
    .store(in: &cancellables)

3. Subscription Lifetime: Always .store(in:)

// CORRECT
apiClient.request(...)
    .sink { completion in }
    receiveValue: { value in }
    .store(in: &cancellables)

// WRONG
apiClient.request(...)
    .sink { completion in }  // Subscription deallocated immediately
    receiveValue: { value in }
// Missing .store()

4. Error Handling in sink()

// CORRECT: Handle completion + value separately
.sink(
    receiveCompletion: { [weak self] completion in
        if case .failure(let error) = completion {
            self?.error = error.localizedDescription
        }
    },
    receiveValue: { [weak self] data in
        self?.data = data
    }
)

// OK: Ignore completion
.sink { [weak self] data in
    self?.data = data
}

// WRONG: Ignore errors
.sink(receiveCompletion: { _ in })  // Silent failure

5. Type Erasure: Always .eraseToAnyPublisher()

// CORRECT: Hide internal type from caller
func fetchPokemon() -> AnyPublisher<[Pokemon], Error> {
    apiClient.request(endpoint: .list)
        .map { /* transform */ }
        .eraseToAnyPublisher()
}

// WRONG: Expose internal chain type
func fetchPokemon() -> Publishers.Map<
    URLSession.DataTaskPublisher,
    [Pokemon]
> {
    // Too specific, changes break callers
}

6. CombineLatest for Multi-Input Filters

// CORRECT: Reactive filtering
Publishers.CombineLatest3($searchText, $selectedType, $pokemon)
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .map { search, type, all in
        all.filter { /* conditions */ }
    }
    .assign(to: &$filteredPokemon)

// WRONG: Manual observer pattern
$searchText.sink { [weak self] text in
    self?.updateFilters()  // Called 3 times separately
}
$selectedType.sink { [weak self] type in
    self?.updateFilters()
}
// Inefficient, unpredictable order

7. Parallel Requests: Use .zip()

// CORRECT: Wait for both
detailPublisher
    .zip(speciesPublisher)
    .map { detail, species in
        PokemonDetail(from: detail, species: species)
    }

// WRONG: Sequential nested
detailPublisher
    .flatMap { detail in
        speciesPublisher.map { species in
            (detail, species)
        }
    }
// Slower, harder to read

Code Organization

File Structure (Top to Bottom)

import Foundation
import Combine
import UIKit  // Only in Presentation

// MARK: - Class/Struct Definition
class ViewModel {
    // MARK: - Published State
    @Published var state: String
    
    // MARK: - Properties
    private let repository: RepositoryProtocol
    
    // MARK: - Init
    init(repository: RepositoryProtocol) { }
    
    // MARK: - Lifecycle (VCs only)
    override func viewDidLoad() { }
    
    // MARK: - Public Methods
    func loadData() { }
    
    // MARK: - Private Methods
    private func setupBindings() { }
    
    // MARK: - Combine Pipeline
    private func setupFilterPipeline() { }
}

Comments (Sparse, Self-Documenting)

// GOOD: Explains WHY
/// Load paginated results with debounce to avoid excessive requests
func loadMore() { }

// BAD: States obvious
// Set isLoading to true
isLoading = true

Access Control

Keyword Usage
public Never in app (single-target, not library)
internal Default, omit keyword
fileprivate Rarely, prefer private with extension
private Default for properties/methods

Example:

final class PokemonListViewModel {
    @Published var pokemon: [Pokemon] = []  // internal
    private var cancellables = Set<AnyCancellable>()
    
    func loadPokemon() { }  // internal
    private func setupFilterPipeline() { }
}

Type Annotations

  • Explicit for public/fileprivate properties
  • Infer for local variables when obvious
// GOOD
@Published var pokemon: [Pokemon] = []
let imageURL: URL? = nil
var filtered = pokemon.filter { $0.name.contains(query) }  // Inferred OK

// BAD
let x = 5  // What is x?
var pokemon = [] // Type unknown

Error Handling

  • Use typed error enums (APIError) in Data layer
  • Use Error protocol in Domain (generic)
  • Convert to user strings in ViewModel
enum APIError: LocalizedError {
    case invalidURL
    case networkError(Error)
    case decodingError(Error)
    case notFound
    case serverError(Int)
    
    var errorDescription: String? {
        // User-friendly messages
    }
}

Final Classes

Mark classes as final unless designed for subclassing.

final class PokemonRepository { }  // CORRECT
class PokemonRepository { }  // Allows accidental inheritance

No Force Unwrap

// GOOD
if let url = imageURL { /* use */ }
let url = imageURL ?? URL(string: "default")!

// BAD
let url = imageURL!  // Crash if nil

Exceptions: Force unwrap only in initializers with constants.

let baseURL = URL(string: "https://pokeapi.co/api/v2")!  // OK, compile-time constant

Consistency Checklist

  • File name matches main type (PokemonListViewController.swift)
  • No public types (internal default)
  • DTOs stay in Data layer
  • UIKit not in Domain/Data
  • [weak self] in all Combine closures
  • .receive(on: .main) before UI updates
  • .store(in: &cancellables) on all subscriptions
  • .eraseToAnyPublisher() on public publishers
  • Typed errors (APIError enum)
  • final class by default