Small iOS-only app built lookup of popular movies, movie details, deep linking, and clean architecture choices.
- Minimum deployment target: iOS 18.0
Two schemes are provided:
MovieDB: live TMDB-backed modeMovieDB-Test: mock mode, local only data
How they work:
- scheme sets
APP_MODE SceneDelegatechooses live or mock dependency composition from that value
Run in Xcode:
- Open
MovieDB.xcodeproj - Choose a simulator
- Select:
MovieDBfor live API dataMovieDB-Testfor mock/demo mode
- Run
Deep link example:
Test target movieapp://moviedetails/603
Live target movieapp://moviedetails/[movideId]
Run unit tests:
- Xcode:
Product > Test
Unit tests cover the critical paths only:
MoviesListViewModelMovieDetailsViewModel- mock repositories
MoviesListPaginator- shared image loading behavior
Why this coverage exists:
- validates list/details state transitions
- protects pagination behavior
- verifies image decoding + cache hit behavior
- keeps the highest-risk logic testable without over-testing UI layout
- Shows a paginated list of popular movies
- Loads poster images with caching
- Opens a movie details screen
- Supports
movieapp://moviedetails/{movie_id} - Handles loading, success, error, retry, pull to refresh, and no-network states
This directly addresses the assignment requirements around:
- popular movies list
- details screen
- deep link support
- dependency injection
- test coverage
The project is organized by feature and split into Presentation / Domain / Data, with MVVM+C in presentation:
App/: startup, DI, routing, coordinator compositionFeatures/MoviesList: list UI, pagination, repository contracts and implementationsFeatures/MovieDetails: details UI, repository contracts and implementationsShared/: networking, images, logging, common state/errors, shared SwiftUI/UIKit helpers
Why this structure was chosen:
- keeps UIKit and SwiftUI screens isolated from networking details
- makes mock and live implementations swappable
- keeps the coordinator responsible for navigation only
- makes the assignment easy to build top-down and test incrementally
The app follows a lightweight Clean Architecture approach:
- Presentation knows how to render state and react to user input
- Domain defines the feature contracts and core models
- Data implements those contracts using either mocks or the real TMDB API
This means the UI does not know whether data comes from:
MockMoviesListRepositoryLiveMoviesListRepositoryMockMovieDetailsRepositoryLiveMovieDetailsRepository
That separation was useful for this task because the app could be built and reviewed in mock mode first, while preserving the same screen flow and dependency graph later in live mode.
-
S — Single Responsibility
MoviesListViewModelmanages list state and pagination orchestrationMoviesListPaginatoronly tracks page-loading stateDefaultImageLoaderonly loads and caches imagesAppCoordinatoronly owns app-level navigation flow
-
O — Open/Closed
- repositories are extended with new implementations without changing UI code
- example: switching from
MockMovieDetailsRepositorytoLiveMovieDetailsRepositorydoes not require changingMovieDetailsViewModel
-
L — Liskov Substitution
- view models depend on repository protocols, so mock and live repositories can be swapped safely
- same applies to shared image loading through
ImageLoading
-
I — Interface Segregation
- features depend on small focused protocols such as
MoviesListRepository,MovieDetailsRepository, andImageLoading - screens do not depend on broader networking or configuration APIs they do not need
- features depend on small focused protocols such as
-
D — Dependency Inversion
- high-level layers depend on abstractions, not concrete types
- example:
MoviesListViewModeldepends onMoviesListRepository, not onURLSessionor TMDB DTOs - concrete implementations are created in
AppDependencies, which acts as the composition root
-
MVVM+C
ViewController/ SwiftUIViewrenders stateViewModelowns screen behavior and state transitionsCoordinatorowns navigation
-
Repository Pattern
- feature data access is hidden behind repository contracts
- this keeps the domain and presentation layers independent from TMDB transport details
-
Composition Root / Manual DI
AppDependenciescreates and wires concrete implementations in one place- this keeps object creation out of view controllers and view models
-
Strategy-like environment switching
- schemes set
APP_MODE SceneDelegatechooses mock or live composition at startup- same app flow, different implementations underneath
- schemes set
-
Shared service abstraction
ImageLoadingis shared between UIKit and SwiftUI- this avoids duplicate image-loading logic across the two UI technologies
Overall, this was chosen because it solves the assignment requirements in a way that is easy to reason about, easy to swap between mock and live data, and straightforward to test without coupling UI code to networking code.
- dedicated factory/module builders instead of the current light composition-root factory style
- stronger endpoint/request abstraction around TMDB image loading
- more polished loading skeletons instead of simple spinners/placeholders
- broader UI test coverage for deep links and pagination
- stronger formatting/presentation models for details screen data
- optional modularization into separate framework targets if the app grows
- Light mode is enforced intentionally
- The app supports both mock-first development and live API mode
- Image loading is shared across UIKit and SwiftUI through one loader abstraction