iOS Pokedex app demonstrating Combine framework, MVVM, and Clean Architecture patterns using UIKit.
A production-grade learning project that fetches Pokemon data from PokeAPI v2 and displays a paginated grid list, detailed stats, evolution chains, and search/filter capabilities. Zero external networking dependencies—uses native URLSession + Combine for reactive data binding.
Target Audience: iOS developers learning reactive programming and scalable architecture patterns.
- Pokemon Grid List — 2-column infinite-scroll pagination (20 per page)
- Pokemon Detail — Stats bars, abilities, evolution chain, official artwork
- Search — Debounced, client-side name filtering (Combine pipeline)
- Type Filter — Chip bar to filter by type (Fire, Water, Electric, etc.)
- Favorites — Heart toggle to save/unsave locally (UserDefaults)
- Error Handling — Network errors with retry button, empty state fallbacks
| Component | Tool | Version | Purpose |
|---|---|---|---|
| Language | Swift | 5.9+ | Modern, type-safe |
| UI Framework | UIKit | iOS 15+ | Native, no SwiftUI (for learning) |
| Reactive | Combine | Native | Publishers, async data binding |
| Layout | SnapKit | latest | Programmatic Auto Layout |
| Images | Kingfisher | latest | Image loading + HTTP caching |
| Networking | URLSession | Native | Lightweight, Combine-first |
| Package Manager | SPM | Xcode | Native dependency management |
| Storage | UserDefaults | Native | Favorites persistence |
┌─────────────────────────────────────┐
│ PRESENTATION (UIKit + ViewModel) │
│ PokemonListVC, PokemonDetailVC │
└──────────────┬──────────────────────┘
│ calls
┌──────────────▼──────────────────────┐
│ DOMAIN (Pure Swift, Testable) │
│ Entities, UseCases, Protocols │
└──────────────┬──────────────────────┘
│ uses
┌──────────────▼──────────────────────┐
│ DATA (APIClient, Repositories) │
│ DTOs, Network, Local Storage │
└──────────────┬──────────────────────┘
│ requests
┌──────────────▼──────────────────────┐
│ PokeAPI v2 (Public REST) │
└─────────────────────────────────────┘
- Xcode 15+
- Swift 5.9+
- iOS 15.0+ deployment target
-
Clone the repository
git clone git@github.com:minhbi245/Pok-API.git cd Pok-API -
Open Xcode project
open PokeAPI/PokeAPI.xcodeproj
-
Build & Run
- Select a simulator or device
- Press
Cmd + Rto build and run - App launches with Pokemon list screen
PokeAPI/
├── Application/
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ └── DIContainer.swift # Dependency injection setup
│
├── Domain/ # Pure Swift, no external deps
│ ├── Entities/
│ │ ├── Pokemon.swift # List item model
│ │ ├── PokemonDetail.swift # Detail screen model
│ │ ├── PokemonType.swift # Type enum + colors
│ │ ├── PokemonStat.swift # Stat structure
│ │ └── EvolutionChain.swift # Evolution data
│ ├── Repositories/
│ │ ├── PokemonRepositoryProtocol.swift
│ │ └── FavoritesRepositoryProtocol.swift
│ └── UseCases/
│ ├── FetchPokemonListUseCase.swift
│ ├── FetchPokemonDetailUseCase.swift
│ ├── FetchEvolutionChainUseCase.swift
│ └── SearchPokemonUseCase.swift
│
├── Data/
│ ├── Network/
│ │ ├── APIClient.swift # URLSession wrapper
│ │ ├── APIEndpoint.swift # Enum of routes
│ │ └── APIError.swift # Error types
│ ├── DTOs/
│ │ ├── PokemonListResponseDTO.swift
│ │ ├── PokemonDetailDTO.swift
│ │ ├── PokemonSpeciesDTO.swift
│ │ └── EvolutionChainDTO.swift
│ └── Repositories/
│ ├── PokemonRepository.swift # PokeAPI impl
│ └── FavoritesRepository.swift # UserDefaults impl
│
└── Presentation/
├── PokemonList/
│ ├── PokemonListViewController.swift
│ ├── PokemonListViewModel.swift
│ ├── PokemonCell.swift
│ └── TypeFilterView.swift
├── PokemonDetail/
│ ├── PokemonDetailViewController.swift
│ ├── PokemonDetailViewModel.swift
│ ├── DetailHeaderView.swift
│ ├── StatBarView.swift
│ ├── AbilitiesView.swift
│ └── EvolutionChainView.swift
└── Common/
├── BaseViewController.swift
├── UIColor+Hex.swift
├── TypeBadgeView.swift
├── ErrorStateView.swift
└── EmptyStateView.swift
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, _ in
try JSONDecoder().decode(T.self, from: data)
}searchSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { query in ... }Publishers.CombineLatest(searchPublisher, filterPublisher)
.map { search, filter in ... }@Published var pokemonList: [Pokemon] = []
// ViewController observes via sink
viewModel.$pokemonList
.receive(on: DispatchQueue.main)
.sink { [weak self] pokemon in
self?.updateUI(pokemon)
}apiPublisher
.tryMap { dto in try dto.toDomain() }
.mapError { APIError.parse($0) }cancellable = publisher.sink(
receiveCompletion: { completion in ... },
receiveValue: { value in ... }
)- Domain — Pure Swift, framework-agnostic, fully testable
- Data — Concrete implementations, API calls, local storage
- Presentation — UIKit + ViewModel with @Published properties
- Application — Dependency injection container
Every screen observes @Published properties from its ViewModel. Changes flow automatically from network response → ViewModel → UI, eliminating manual KVO or delegation boilerplate.
Network errors display a user-friendly error state with a retry button. Empty states appear when no Pokemon match the search/filter.
Favorites persist locally and survive app restarts. List fetch requires network; detail requires network. Caching is intentionally minimal for learning purposes.
See docs/design-guidelines.md for:
- Official Pokemon type colors
- Typography scale (SF Pro system font)
- Spacing grid (8pt baseline)
- Component specifications (cell, badge, stat bar)
See docs/code-standards.md for:
- Naming conventions (PascalCase files, camelCase properties)
- Combine subscription management
- Error handling patterns
- Comment guidelines
- Tech Stack — Dependency details and design decisions
- System Architecture — Layer breakdown and data flow diagrams
- Code Standards — Naming, patterns, conventions
- Design Guidelines — Colors, typography, component specs
- Project Overview & PDR — Requirements and success metrics
- Project Roadmap — Phase timeline and future work
- Unit Tests — Domain layer fully testable; add XCTest suite
- UI Tests — XCUITest for critical user flows
- CoreData Caching — Persist list responses for offline browsing
- Dark Mode — Extend color palette for appearance API
- SwiftUI Migration — Rewrite screens in SwiftUI while keeping ViewModel logic
MIT License — See LICENSE file for details.
Project Stats:
- 38 Swift files
- ~2,054 lines of code
- 7 completed development phases
- 8+ Combine patterns demonstrated
Perfect for learning Combine reactivity, Clean Architecture, MVVM, and scalable iOS app structure.