Backstage take home assignment. Below is what has been done:
- Search interface for Movies and Restaurants
- Built in UIKit primarily and added SwiftUI views as bonus
I made the app to be primarily UIKit.
I added a local SPM package (Packages/) that holds all the real logic.
This Packages has all of the features abstracted. Each feature has it's own test and target. That way they can be ran independently without building the whole app target.
Backstage (app)
└── SearchViewController ─── SearchViewModel
├── MovieFeature
└── RestaurantFeature
Packages/
├── APIClient - networking, models
├── DesignKit - shared UI (loading, error, empty states)
├── MovieFeature - movie search + VM
└── RestaurantFeature - restaurant search + VM
I went with a delegate-based view model for the UIKit side. SearchViewModel owns both API clients, handles debouncing, fires off parallel searches for movies and restaurants, and tracks all the state (loading, errors, selection). The view controller just listens for delegate callbacks and reloads the table.
The delegate pattern is what UIKit is simple to follow, simple to test, and doesn't need any bridging.
The SwiftUI version uses its own @Observable view models from the same feature packages. Both implementations share the same API clients and models underneath.
Everything goes through protocol abstractions (MovieSearching, RestaurantSearching), so tests can swap in stubs without touching the network.
Backstage/
├── Backstage/App/
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ └── Features/Search/
│ ├── UIKit/
│ │ ├── SearchViewController.swift
│ │ └── SearchViewModel.swift
│ └── SwiftUI/
│ ├── SearchView.swift
│ ├── MovieResultsView.swift
│ └── RestaurantResultsView.swift
├── BackstageTests/
│ ├── SearchViewModelTests.swift
│ └── Mocks.swift
└── Packages/
├── Package.swift
├── Sources/ (APIClient, DesignKit, MovieFeature, RestaurantFeature)
└── Tests/ (APIClientTests, DesignKitTests, MovieFeatureTests, RestaurantFeatureTests)
One SPM package with multiple targets instead of separate packages. Simpler to manage -- one Package.swift, compile-time boundary checks between targets. The downside is you can't version them independently, but for an app this size that doesn't matter.
Dedicated SearchViewModel instead of using the package-level VMs directly. The package VMs (MovieSearchViewModel, RestaurantSearchViewModel) are @Observable and designed for SwiftUI. Rather than awkwardly observing them from UIKit, SearchViewModel talks to the API clients directly and owns all the state in one place.