A modern iOS application showcasing Harry Potter characters using Kotlin Multiplatform (KMP) for shared business logic and SwiftUI for the user interface.
iPhoneDemo.mp4
iPadDemo.mp4
- Character Browsing: View all Harry Potter characters with detailed information
- Smart Filtering: Filter by All, Students, or Staff members
- Favorites: Mark characters as favorites with segmented control filtering
- Character Details: Rich detail view with character information, wand details, and more, including caching for character images
- House Selection: Choose your Hogwarts house (Gryffindor, Slytherin, Ravenclaw, Hufflepuff)
- Profile Photo: Capture and set a profile picture using device camera
- Kotlin Multiplatform: Shared networking, business logic, and data layer
- SwiftUI: Modern declarative UI with Swift 6 concurrency support
- MVVM Architecture: Clean separation of concerns
- Type-Safe Networking: Custom
NetworkResultwrapper for robust error handling - Offline-First Ready: Architecture supports caching (not implemented due to time)
HpTest-KMP-iOS/
├── shared/ # Kotlin Multiplatform Module
│ └── src/commonMain/kotlin/
│ ├── data/
│ │ ├── api/ # Network layer (Ktor client)
│ │ └── repository/ # Repository pattern
│ ├── domain/
│ │ ├── models/ # DTOs (CharacterDTO, WandDTO)
│ │ └── filters/ # Business logic (CharacterFilter)
│ └── utils/ # NetworkResult wrapper
│
└── iosApp/HpTest/ # iOS Application
├── Core/
│ ├── Managers/ # Business logic managers
│ │ ├── FavoritesManager # Favorites state management
│ │ ├── HouseManager # House selection state
│ │ └── ProfileImageStore # Profile photo persistence
│ └── Models/ # iOS domain models
│
└── Features/
├── Characters/ # Characters feature
│ ├── CharactersListView # Master list view
│ ├── CharactersViewModel # ViewModel with KMP integration
│ ├── CharacterRowView # List row component
│ └── CharacterDetailView # Detail view
└── CameraPicker # Camera integration
This challenge intentionally uses:
- KMP for networking, domain, and shared business logic
- Native SwiftUI for platform-native UX and faster iteration
This approach keeps business logic reusable while preserving the best Apple-platform user experience.
Shared (KMP)
- Language: Kotlin 1.9.21
- Networking: Ktor 2.3.7 (with automatic retry, timeout handling)
- Serialization: kotlinx.serialization 1.6.2
- Concurrency: Kotlin Coroutines 1.7.3
iOS
- Language: Swift 6
- UI Framework: SwiftUI
- Minimum iOS Version: iOS 15+
- Concurrency: Swift Concurrency (async/await, @MainActor)
- macOS with Xcode 15.0 or later
- Java 17+ (for Gradle)
- Xcode Command Line Tools:
xcode-select --install
-
Clone the repository
git clone <repository-url> cd HpTest-KMP-iOS
-
Build the KMP framework
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
For other architectures:
# Intel Mac simulator ./gradlew :shared:linkDebugFrameworkIosX64 # Physical device ./gradlew :shared:linkDebugFrameworkIosArm64
-
Open in Xcode
open iosApp/HpTest.xcodeproj
-
Run the app
- Select a simulator or device
- Press
Cmd + Rto build and run
The Xcode project includes a Run Script Phase that automatically builds the KMP framework:
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcodeThis runs automatically when building from Xcode and selects the correct framework for your target (simulator vs device).
Base URL: https://hp-api.onrender.com
Endpoint Used: /api/characters
Note: This API can be slow on cold starts (free tier). The app includes:
- No Pagination: All characters loaded at once
- 30-second request timeout
- Automatic retry logic (3 attempts with exponential backoff)
- User-friendly error messages
- Decided to use Characters endpoint because it contains all data, and then filter data in session
Why KMP?
- Code Reuse: Share networking, business logic, and data models
- Type Safety: Kotlin's type system prevents runtime errors
- Platform Native: iOS app uses 100% native SwiftUI
What's Shared?
- ✅ Network client (Ktor with retry/timeout)
- ✅ Repository pattern
- ✅ Data models (DTOs)
- ✅ Business logic (filtering)
- ✅ Error handling (NetworkResult)
What's iOS-Specific?
- ✅ UI layer (SwiftUI)
- ✅ Navigation
- ✅ State management (Managers)
- ✅ Camera integration
- ✅ User preferences
The app is fully compatible with Swift 6's strict concurrency checking:
@MainActorisolation for ViewModels and UI state- No callback-based APIs (KMP uses property-based API)
- Sendable types across boundaries
Favorites & House Selection: SwiftUI Environment + @Observable
@Environment(\.favoritesManager) private var favoritesManager
@Environment(\.houseManager) private var houseManagerData Loading: MVVM pattern with ViewModel calling KMP repository
@MainActor
@Observable
class CharactersViewModel {
private let repository = CharacterRepository()
// ...
}- Master-Detail Navigation: NavigationSplitView for iPad-optimized layout
- Segmented Control: Quick switching between All/Students/Staff/Favorites
- Pull-to-Refresh: Refresh character list
- Empty States: ContentUnavailableView for no selection / no favorites
- Loading States: ProgressView during data fetch
- Error Handling: User-friendly error messages with retry option
- Missing: Test coverage for ViewModels, Managers, and KMP code
- Priority: High
- Effort: 2-3 hours
- Implementation:
// ViewModel tests @Test func testFetchCharactersSuccess() async { let viewModel = CharactersViewModel() await viewModel.loadCharacters() #expect(viewModel.characters.isEmpty == false) } // KMP tests class CharacterRepositoryTest { @Test suspend fun `fetchCharacters returns success`() { val repository = CharacterRepository() val result = repository.fetchCharacters() assertTrue(result is NetworkResult.Success) } }
- Current: NavigationSplitView handles navigation
- Improvement: Coordinator pattern for complex navigation flows
- Priority: Medium
- Effort: 3-4 hours
- Benefits:
- Centralized navigation logic
- Easier to test navigation flows
- Deep linking support
- Current: Network-only data fetching
- Improvement: Cache characters locally
- Priority: High
- Effort: 4-5 hours
- Implementation:
- SwiftData or CoreData for iOS
- SQLDelight in KMP for shared DB
- Cache invalidation strategy
- Current: Filter by role only
- Improvement: Full-text search by name, house, actor
- Priority: Medium
- Effort: 2 hours
- Implementation:
.searchable(text: $searchText, prompt: "Search characters...") var filteredCharacters: [Character] { characters.filter { searchText.isEmpty || $0.name.localizedCaseInsensitiveContains(searchText) } }
- Current: Camera picker exists, but no dedicated profile screen
- Improvement: Standalone profile view with:
- Profile photo display
- House selection UI
- User preferences
- Priority: Medium
- Effort: 2 hours
- API Available:
/api/spellsendpoint exists - Improvement: Add spells browsing section
- Priority: Low
- Effort: 3-4 hours
- Current: Basic SwiftUI accessibility
- Improvement:
- VoiceOver labels
- Dynamic Type support
- Accessibility identifiers for UI testing
- Priority: High
- Effort: 2-3 hours
- Current: Default SwiftUI transitions
- Improvement:
- Custom transitions for navigation
- Loading shimmer effects
- Favorite button animation
- Priority: Low
- Effort: 2-3 hours
- Current: Error message with manual retry
- Improvement:
- Automatic retry with exponential backoff UI feedback
- Offline mode detection
- Network reachability monitoring
- Priority: Medium
- Effort: 2 hours
- Current: iOS uses KMP DTOs directly
- Issue: Tight coupling to API structure
- Fix: Create iOS domain models
// iOS Domain Model
struct Character {
let id: String
let name: String
// ... iOS-specific computed properties
init(from dto: CharacterDTO) {
self.id = dto.id
self.name = dto.name
// ...
}
}- Current: Direct instantiation of repositories
- Improvement: Protocol-based DI for testability
protocol CharacterRepositoryProtocol {
func fetchCharacters() async -> NetworkResult<[CharacterDTO]>
}
class CharactersViewModel {
private let repository: CharacterRepositoryProtocol
init(repository: CharacterRepositoryProtocol = CharacterRepository()) {
self.repository = repository
}
}- Missing: Structured logging, crash reporting
- Tools: OSLog, Firebase Crashlytics, Sentry
- Priority: Medium
- Effort: 1-2 hours
- Development Time: Approximately 4 hours (as per challenge guidelines)
- Focus Areas: Clean architecture, KMP integration, Swift 6 compatibility
- Tradeoffs: Features vs code quality vs time - prioritized architecture and core features
This is a coding challenge project. All Harry Potter content is owned by Warner Bros. Entertainment Inc.
- API: Harry Potter API by @maael
- Framework: Kotlin Multiplatform by JetBrains
- UI: SwiftUI by Apple