Skip to content

A simple Pokedex app written in SwiftUI that implements the PokeAPI, using Swift Concurrency, MVVM architecture and pagination

License

Notifications You must be signed in to change notification settings

brillcp/PokedexUI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

icon

swift release platforms spm license stars

PokedexUI

PokedexUI is a modern example app built with SwiftUI by Viktor GidlΓΆf. It integrates with the PokeAPI to fetch and display PokΓ©mon data using a clean, reactive architecture using async / await and Swift Concurrency.

pd1 pd2

Architecture πŸ›

PokedexUI implements a Protocol-Oriented MVVM architecture with Clean Architecture principles. It features generic data fetching, SwiftData persistence, and reactive UI updates using Swift's @Observable macro.

Key Architectural Benefits

  • βœ… Protocol-Oriented: Enables dependency injection and easy testing
  • βœ… Generic Data Flow: Unified pattern for all data sources
  • βœ… Storage-First: Offline-capable with automatic sync
  • βœ… Actor-Based Concurrency: Thread-safe data operations
  • βœ… Clean Separation: Clear boundaries between layers
  • βœ… Type Safety: Compile-time guarantees via generics
  • βœ… Reactive UI: Automatic updates via @Observable

SOLID Compliance Score: 0.92 / 1.0

  • Single Responsibility: Each component has a focused purpose
  • Open/Closed: Extensible via protocols without modification
  • Liskov Substitution: Protocol conformance ensures substitutability
  • Interface Segregation: Focused, cohesive protocols
  • Dependency Inversion: Depends on abstractions, not concretions

View Layer πŸ“±

The root PokedexView is a generic view that accepts protocol-conforming ViewModels, enabling dependency injection and testability:

struct PokedexView<
    PokedexViewModel: PokedexViewModelProtocol,
    ItemListViewModel: ItemListViewModelProtocol,
>: View {
    @State var viewModel: PokedexViewModel
    let itemListViewModel: ItemListViewModel
    
    var body: some View {
        TabView(selection: $viewModel.selectedTab) {
            Tab(Tabs.pokedex.title, systemImage: viewModel.grid.icon, value: Tabs.pokedex) {
                PokedexContent(viewModel: $viewModel)
            }
            // Additional tabs...
        }
        .applyPokedexConfiguration(viewModel: viewModel)
    }
}

ViewModel Layer 🧾

Protocol-Oriented Design

ViewModels conform to protocols, enabling flexible implementations and easier testing:

protocol PokedexViewModelProtocol {
    var pokemon: [PokemonViewModel] { get }
    var isLoading: Bool { get }
    var selectedTab: Tabs { get set }
    var grid: GridLayout { get set }
    
    func requestPokemon() async
    func sort(by type: SortType)
}

Generic Data Fetching

The DataFetcher protocol provides a unified pattern for storage-first data loading:

protocol DataFetcher {
    associatedtype StoredData
    associatedtype APIData  
    associatedtype ViewModel
    
    func fetchStoredData() async throws -> [StoredData]
    func fetchAPIData() async throws -> [APIData]
    func storeData(_ data: [StoredData]) async throws
    func transformToViewModel(_ data: StoredData) -> ViewModel
    func transformForStorage(_ data: APIData) -> StoredData
}

extension DataFetcher {
    func fetchDataFromStorageOrAPI() async -> [ViewModel] {
        // Storage-first approach with API fallback
        guard let localData = await fetchStoredDataSafely(), !localData.isEmpty else {
            return await fetchDataFromAPI()
        }
        return localData.map(transformToViewModel)
    }
}

Concrete Implementation

The PokedexViewModel implements both protocols:

@Observable
final class PokedexViewModel: PokedexViewModelProtocol, DataFetcher {
    private let pokemonService: PokemonServiceProtocol
    private let storageReader: DataStorageReader
    
    var pokemon: [PokemonViewModel] = []
    var isLoading: Bool = false
    
    func requestPokemon() async {
        guard !isLoading else { return }
        pokemon = await withLoadingState {
            await fetchDataFromStorageOrAPI()
        }
    }
}

Data Layer πŸ“¦

SwiftData Persistence

DataStorageReader provides a generic actor-based interface for SwiftData operations:

@ModelActor
actor DataStorageReader {
    func store<M: PersistentModel>(_ models: [M]) throws {
        models.forEach { modelContext.insert($0) }
        try modelContext.save()
    }
    
    func fetch<M: PersistentModel>(
        sortBy: SortDescriptor<M>
    ) throws -> [M] {
        let descriptor = FetchDescriptor<M>(sortBy: [sortBy])
        return try modelContext.fetch(descriptor)
    }
}

Intelligent Search System πŸ”

A high-performance, protocol-driven search implementation with sophisticated multi-term filtering and real-time results.

Search Architecture

The search system follows the same unified DataFetcher pattern, ensuring consistent data loading and offline capabilities:

@Observable
final class SearchViewModel: SearchViewModelProtocol, DataFetcher {
    var pokemon: [PokemonViewModel] = []
    var filtered: [PokemonViewModel] = []
    var query: String = ""
    
    func loadData() async {
        pokemon = await fetchDataFromStorageOrAPI() // Uses unified data fetching
    }
}

Advanced Filtering Algorithm

Multi-Term Processing & Matching

func updateFilteredPokemon() {
    let queryTerms = query
        .split(whereSeparator: \.isWhitespace)  // Split on whitespace
        .map { $0.normalize }                   // Diacritic-insensitive
        .filter { !$0.isEmpty }
    
    filtered = pokemon.filter { pokemonVM in
        let name = pokemonVM.name.normalize
        let types = pokemonVM.types.components(separatedBy: ",").map { $0.normalize }
        
        return queryTerms.allSatisfy { term in
            name.contains(term) || types.contains(where: { $0.contains(term) })
        }
    }
}

Key Features

  • βœ… Real-time Filtering: Results update instantly as you type
  • βœ… Multi-term Support: "fire dragon" finds PokΓ©mon matching both terms
  • βœ… Type-aware Search: Find by type (e.g., "water", "electric") or name
  • βœ… Diacritic Insensitive: Handles accented characters automatically
  • βœ… Storage Integration: Searches local SwiftData with API fallback

The search algorithm ensures all terms must match for precise results while supporting partial name matching and type combinations.

Sprite Loading & Caching 🎨

Asynchronous image loading with intelligent caching:

actor SpriteLoader {
    func loadSprite(from urlString: String) async -> UIImage? {
        // Check cache first, then network with automatic caching
    }
}

Dependencies πŸ”—

PokedexUI uses the HTTP framework Networking for all the API calls to the PokeAPI. You can read more about that here. It can be installed through Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/brillcp/Networking.git", .upToNextMajor(from: "0.9.3"))
]

Requirements ❗️

  • Xcode 15+
  • iOS 17+ (for @Observable and SwiftData)
  • Swift 5.9+