SwiftUI app for the Rick & Morty REST API. Built as a take-home; aimed at a Clean-Architecture layout that's easy to navigate.
brew install xcodegen
xcodegen generate
open RickAndMortyiOS.xcodeprojOr from the command line:
xcodebuild -project RickAndMortyiOS.xcodeproj \
-scheme RickAndMortyiOS \
-destination 'platform=iOS Simulator,name=iPhone 16' \
testXcode 16, iOS 17 simulator. The .xcodeproj is generated from project.yml and gitignored, so reviewers can read the target layout in one place and there are no merge conflicts on the project file.
No third-party dependencies. Networking, image loading, persistence, and the snapshot-style tests all use Apple frameworks (URLSession, NSCache, URLCache, SwiftData, ImageRenderer).
Required
- Paginated character list, infinite scroll (loads when within 5 rows of the end)
- Pull-to-refresh
- Search by name with 350 ms debounce
- Detail screen with all character fields + the episodes they appear in (batched as
/episode/[1,2,3])
Nice-to-haves (all in)
- Favourites with SwiftData, exposed as an
AsyncStream<Set<Int>>so both the list and detail screens stay in sync without polling - Offline cache: page 1 of the unfiltered list is written to
Caches/CharacterCache/and served as a fallback when the device is offline - Status filter chips (Alive / Dead / Unknown) combinable with search
- Skeleton/shimmer loaders for the list and detail
- Dark Mode via semantic colours
- Render-smoke tests for the row and status badge (no third-party snapshot lib, no recorded PNGs to maintain)
App/ composition root + entry point
Domain/ pure Swift -- models, use cases, repository protocols, DomainError
Data/ URLSession, DTOs, mappers, repository impls, caches, SwiftData
Presentation/ SwiftUI views + @Observable view models, theme, components
Dependency rule is the usual: Presentation -> Domain <- Data. Domain only imports Foundation (for URL).
The composition root is AppDependencies. It builds the URLSession, repos, use cases, and SwiftData container once and exposes two factory methods (makeCharacterListViewModel, makeCharacterDetailViewModel) plus the image loader. No singletons in production. Tests assemble their own.
Everything that owns mutable state (network repo, episode memo, favourites, image loader, page cache) is an actor. View models are @MainActor @Observable.
HTTPClient is a one-method protocol that URLSession conforms to via an extension. Tests pass a MockHTTPClient instead.
All transport-level failures are translated to DomainError inside DefaultNetworkService. The presentation layer never sees a URLError, an HTTP status code, or a DecodingError. Mapping:
- 404 ->
.notFound(search shows the empty state, not a generic error) - 5xx ->
.server(code) - offline / lost connection ->
.offline(falls back to disk cache for page 1) - timeout / 408 ->
.timedOut - decode failure ->
.decoding
The list distinguishes "first load failed" (full-screen retry) from "next page failed" (footer message that doesn't blow away existing rows).
35 tests, all green out of the box. Run with xcodebuild ... test or Cmd-U in Xcode.
NetworkServiceTests-- decode + the five error-mapping pathsCharacterMapperTests-- DTO -> domain, including unknown enum valuesFetchCharactersUseCaseTests-- forwarding, invalid page, error propagationEpisodeRepositoryImplTests-- single/array decode, ordering, memoizationSwiftDataFavouritesRepositoryTests-- toggle, AsyncStream broadcast, persistence across instancesCharacterListViewModelTests-- load, pagination, debounced search, favouritesCharacterDetailViewModelTests-- load, error path, toggle, cancellation cleanupCharacterRowSnapshotTests-- row + status-badge render-smoke
MockHTTPClient lets the network tests run real fixture JSON (see RickAndMortyiOSTests/Fixtures/) without hitting the network. The view-model tests use small actor mocks for the repositories.
- iOS 17 deployment target. I leaned on
@Observable+ SwiftData, both 17-only. Spec said 16+. Easiest fix is an#availableshim that falls back toObservableObject+UserDefaultson iOS 16. - Render-smoke instead of pixel-diff snapshot tests. Skipped pulling in a snapshot library to keep the project dependency-free; the views, layout, and stub image loader still execute on every test run, just no byte comparison against checked-in PNGs. Would swap in a real diffing library if I had one approved.
- No UI tests. Would add an
XCUIApplicationsmoke test that boots the app against a stubbedURLProtocoland walks the list -> detail -> favourite flow. - Image loader is in-house. Small actor with request coalescing +
NSCache+URLCache. Fine here; would benchmark against Nuke for a real product. - Favourites are id-only. A future "Favourites" tab would have to refetch. With more time I'd denormalise the relevant fields into the SwiftData model so it works fully offline.
- Logging via
os.Logger(AppLog.networketc.) is in; analytics/crash reporting/feature flags are not. The DI container is the seam for those.