This repository serves its purpose as comprehensive iOS project demonstrating current best practices and modern iOS development approaches. It tries to replicate as many best practices as possible across mulitple iOS architectures and free public APIs. Testflight: https://testflight.apple.com/join/FaXX2mUY
How to enable Pokedex on TestFlight version
Please search for a Pokemon film and click on "Pocket Monster" keyword from detail page to enable Pokedex tab.
Getting started
Create a xcconfig file with this pattern in Build.xcconfig:TMDB_API_KEY=
PRODUCT_BUNDLE_IDENTIFIER=
Also adding a GoogleService-Info.plist file to the root of the project for Firebase Analytics. (Or setting the false flag in Rebuild/Rebuild/RebuildApp.swift to skip Firebase Analytics)
What you'll find looking at this repo:
- feature based modularization with Swift packages
- VIPER, Clean & MVVM, SwiftUI & UIKit
- REST API & GraphQL networking & OAuth
- SwiftGen, SwiftLint, SwiftFormat
- GitHub CI build & test
| Screen name | Home screen |
|---|---|
![]() |
![]() |
| Reference | My TMDB_Discover module |
- Secure Credential Management
- β API keys stored in GitHub Secrets for CI/CD and read when running workflows (see .github/workflows/ci.yml)
- β
Sensitive tokens (
session_idhere) stored in iOS Keychain after authentication (see TMDB_Shared_Backend module)
π§ Testing:
- β
100% code coverage for TMDB_Discover module. For testabbility purpose:
- Data layer & domain layer should be wrapped in protocol for easy mocking. Mocking
URLProtocolis for testing URLSession Task creation. - Using ViewInspector library for SwiftUI unit testing.
- Data layer & domain layer should be wrapped in protocol for easy mocking. Mocking
- β
Code Generation
- π΄ Sourcery for mock generation
- β SwiftGen for type-safe assets and localizations (Note: SwiftGen still not supporting Xcode 15 String catalog so this project still use .strings files.)
- β
Linting & Formatting: run
swiftformat .orswift package plugin swiftlintat project root level- SwiftFormat and SwiftLint are also run as "run script phase" in Xcode build settings.
- β
SwiftLint is also run as a GitHub Action workflow, Linting on every pull request. (SwiftLint doesn't work well with
// swiftlint:disablecomments on Ubuntu)
- π§ UI Development
- β
SwiftUI Previews for all UI components, including
UIViewandUIViewController,- π§ Preview should be covering all states from view models.
- π§ Demo implementations for all UI modules
- β
SwiftUI Previews for all UI components, including
- β
Support multiple themes:
- β Light theme
- β Dark theme
- β Sepia theme
- β System theme
Tracking:
- β
Using Firebase Analytics for tracking events with abstraction to avoid direct dependency of each module.
- GoogleService-Info.plist is ignored by Git and should be set as a secret in GitHub.
Using GitHub Actions for CI/CD.
- β
ios.yml and test.sh files can generate coverage report for modules with tests. 3 actions workflow:
- SwiftLint run
- Build project
- Test pre-defined schemes on available simulators
- π΄ upload to TestFlight (or BrowserStack, Remote Testkit, etc.)
Setup on GitHub for team collaboration:
- β
automatically running unit tests and code coverage on newly opened pull requests. (Due to SwiftPM limitation mentioned above, test.sh using a custom command of
xcrun llvm-covinstead of a normal xctestplan file) - π§ Block merging pull requests unless certain conditions are met (e.g. code coverage is 100%, 2 approvals from other team members, etc.)
Progress status is classified as: β Finished π§ In Progress π΄ Not Started π Finished but needs updates
Using Router Pattern, NavigationStack and Coordinator pattern
The TMDB section uses a Coordinator pattern with NavigationStack for routing between screens. Below is a comprehensive matrix showing navigation capabilities:
Tab Routes (Root Level)
- Movie Feed (
movieFeed) - Marketplace/Discover (
marketplace) - Profile (
profile)
Navigation Destinations
| From Screen | To Screen | Route Type | Status | Notes |
|---|---|---|---|---|
| Movie Feed | Movie Detail | TMDBRoute.movieDetail |
β | Tap on movie in feed |
| Movie Feed | TV Show Detail | TMDBRoute.tvShowDetail |
β | Tap on TV show in feed |
| Movie Feed | Movie List (Filtered) | TMDBRoute.movieList |
β | Apply filters/search |
| Marketplace/Discover | Movie Detail | TMDBRoute.movieDetail |
β | Tap on trending movie |
| Marketplace/Discover | TV Show Detail | TMDBRoute.tvShowDetail |
β | Tap on trending TV show |
| Marketplace/Discover | TV Show List | TMDBRoute.tvShowList |
β | Tap genre/cast/on-the-air |
| Profile | Movie Detail | TMDBRoute.movieDetail |
β | Tap favorite/watchlist movie |
| Profile | TV Show Detail | TMDBRoute.tvShowDetail |
β | Tap favorite/watchlist TV show |
| Movie Detail | Movie List (by Keyword) | TMDBRoute.movieList(.keyword) |
β | Tap keyword tag |
| Movie Detail | Cast/Crew Detail | π΄ | Not implemented | No route for person detail |
| TV Show Detail | Season Detail | π΄ | Not implemented | Internal to TVShowDetail |
| TV Show Detail | Cast Detail | π΄ | Not implemented | No route for person detail |
| TV Show List | TV Show Detail | TMDBRoute.tvShowDetail |
β | Tap TV show in filtered list |
| Movie List | Movie Detail | TMDBRoute.movieDetail |
β | Tap movie in filtered list |
| Any Screen | Person/Cast Detail | π΄ | Missing | Would need TMDBRoute.personDetail |
Navigation Parameters
-
TMDBRoute.movieDetail(MovieRouteModel)- Required: movie ID
- Optional: title, overview, poster path, backdrop path, vote average, etc.
-
TMDBRoute.tvShowDetail(Int)- Required: TV show ID
-
TMDBRoute.movieList(AdditionalMovieListParams)- Supports: keyword filtering, genre filtering, cast filtering
- Example:
.movieList(.keyword(123))
-
TMDBRoute.tvShowList(TVShowFeedType)- Supports:
.onTheAir,.discoverWithGenre,.discoverWithTVGenre,.discoverWithCast
- Supports:
Missing Navigation Routes
- π΄ Person/Cast Detail page (tap on actor/crew member)
- π΄ Season Detail page (separate from TV show detail)
- π΄ Episode Detail page
- π΄ Reviews/Comments page
- π΄ Similar Movies/TV Shows (currently might be handled via movieList/tvShowList)
β Using Swift Package Manager to manage dependencies and only leaving a thin app shell using Xcode project. This is way more Git friendly than using Xcode project. However, it's still debatable if this is better than using new XC16 buildable folders (Package.swift at root level is also difficult to setup on latest Xcode version.). - Pros: Swift, readable & Git friendly - Cons: code suggestions, previews not as smooth as using Xcode project, coverage report also not ignoring test files.
Swinject are used for dependency injection.
Details of packages:
- TMDB_MVVM_Detail: Showing details of a movie including overview, cast, crew, keywords, etc. Using SwiftUI and MVVM architecture.
- TMDB_Discover: Display list of TV shows (from "up in the air" or "airing today" TMDB API) using Clean Architecture and SwiftUI.
- TMDB_Feed: same as TMDB_Discover but using MVVM architecture. Also supports endless loading
- TMDB_Clean_Profile: Handling authentication and displaying user profile (including avatar, favourite movies & TV shows & watchlist). Using Clean Architecture and UIKit and Combine for concurrency.
- TMDB_TVShowDetail: Showing details of a TV show including overview, cast, crew, TV seasons. Using SwiftUI and Data Store pattern. Support multiple themes.
- Pokedex_Pokelist: loading a Pokemon list from Pokedex GraphQL API. Using VIPER architecture and UIKit
- Pokedex_Detail: loading a Pokemon detail from Pokedex GraphQL API. Using RxSwift & RxCocoa for reactive programming. MVVM architecture.
Below are the folder structures showing the architectural layers for each module:
Pokedex (Coordinator Pattern)
Pokedex/
βββ PokedexView.swift
βββ Router/
βββ PokelistRouter.swift
Pokedex_Detail (MVVM)
Pokedex_Detail/
βββ View/
β βββ PokemonDetailViewController.swift
β βββ PokemonContentDetailViewController.swift
β βββ LoadingViewController.swift
βββ ViewModel/
βββ PokemonDetailViewModel.swift
Pokedex_Pokelist (VIPER)
Pokedex_Pokelist/
βββ Entities/
β βββ PokemonEntity.swift
βββ Interactor/
β βββ PokelistInteractor.swift
βββ Presenter/
β βββ PokelistPresenter.swift
βββ View/
β βββ PokelistViewController.swift
β βββ PokemonCell.swift
βββ Protocols/
βββ PokelistProtocols.swift
TMDB_Discover (Clean Architecture)
TMDB_Discover/
βββ app/ (DI)
β βββ DiscoverAssembly.swift
βββ data/
β βββ datasource/
β βββ repositories/
βββ domain/
β βββ entities/
β βββ repositories/
β βββ usecase/
βββ presentation/
βββ pages/
βββ view_models/
βββ widgets/
TMDB_Feed (MVVM)
TMDB_Feed/
βββ Backend/
β βββ APIServiceProtocol.swift
βββ Model/
β βββ MovieModel.swift
β βββ MovieListResponse.swift
βββ ViewModels/
β βββ MovieFeedViewModel.swift
β βββ TVShowFeedViewModel.swift
βββ Views/
βββ Pages/
βββ Widgets/
TMDB_MovieDetail (MVVM)
TMDB_MovieDetail/
βββ Model/
β βββ Movie.swift
β βββ People.swift
β βββ Genre.swift
βββ ViewModels/
β βββ MovieDetailViewModel.swift
β βββ MovieCastingViewModel.swift
β βββ MovieWatchProvidersViewModel.swift
βββ Views/
βββ Pages/
βββ Sections/
βββ StaticViews/
TMDB_Profile (Clean Architecture)
TMDB_Profile/
βββ DI/
β βββ ProfileAssembly.swift
βββ Data/
β βββ Repositories/
βββ Domain/
β βββ Entities/
β βββ Repositories/
β βββ UseCases/
βββ Presentation/
βββ controllers/
βββ ViewModels/
βββ Views/
TMDB_TVShowDetail (Data Store Pattern - SwiftUI)
TMDB_TVShowDetail/
βββ Views/
β βββ TVShowDetailView.swift
β βββ TVShowDetailContentView.swift
β βββ (Component views)
βββ Resources/












