From 0beaeee25cda41898904c9c1bcbd434079905d9e Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:32:43 +0530 Subject: [PATCH 01/16] feat: implement Store architecture and dynamic Koin dependency resolver --- iosApp/iosApp/Bridge/FlowCollector.swift | 25 ++++++++ iosApp/iosApp/Bridge/KoinHelper.swift | 74 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 iosApp/iosApp/Bridge/FlowCollector.swift create mode 100644 iosApp/iosApp/Bridge/KoinHelper.swift diff --git a/iosApp/iosApp/Bridge/FlowCollector.swift b/iosApp/iosApp/Bridge/FlowCollector.swift new file mode 100644 index 0000000..2c58c06 --- /dev/null +++ b/iosApp/iosApp/Bridge/FlowCollector.swift @@ -0,0 +1,25 @@ +import shared + +/// Bridges a KMP `FlowCollector` protocol so we can collect +/// Kotlin `StateFlow` emissions from Swift. +/// +/// Usage: +/// ```swift +/// try await stateFlow.collect( +/// collector: SwiftFlowCollector { value in ... } +/// ) +/// ``` +final class SwiftFlowCollector: Kotlinx_coroutines_coreFlowCollector { + + private let onEmit: (Any?) -> Void + + init(_ onEmit: @escaping (Any?) -> Void) { + self.onEmit = onEmit + } + + // KMP exports `emit(value:)` as an async function + // via the ObjC completion-handler bridge. + func emit(value: Any?) async throws { + onEmit(value) + } +} diff --git a/iosApp/iosApp/Bridge/KoinHelper.swift b/iosApp/iosApp/Bridge/KoinHelper.swift new file mode 100644 index 0000000..8e061d2 --- /dev/null +++ b/iosApp/iosApp/Bridge/KoinHelper.swift @@ -0,0 +1,74 @@ +import shared + +/// Thin Swift wrapper around the Koin DI container. +/// Call `KoinHelper.start()` once at app launch, then resolve +/// dependencies through the static helpers. +@MainActor +enum KoinHelper { + + // MARK: - Bootstrap + + private static var isStarted = false + + /// Initialise the shared Koin graph. + static func start() { + guard !isStarted else { return } + isStarted = true + + ModulesKt.doInitKoin( + driverFactory: DatabaseDriverFactory(), + enableNetworkLogs: true, + appDeclaration: { app in + self.koinApp = app + app.modules(modules_: [ModulesKt.viewModelModule]) + + print("--- KOIN MAPPINGS ---") + if let mappings = app.koin.instanceRegistry.instances as? [String: Any] { + for key in mappings.keys { + print("Koin key: \(key)") + } + } + print("---------------------") + } + ) + } + + // MARK: - Resolution + + /// Retrieves a Dependency from Koin by fuzzy-matching the class name in the registry entries + /// to obtain the underlying `SharedKotlinKClass`. This circumvents the fact that ObjC `get()` + /// functions crash when the KClass is erased. + private static func resolve(className: String) -> T { + guard let koinApp = koinApp else { + fatalError("Koin has not been started.") + } + + let koin = koinApp.koin + guard let mappings = koin.instanceRegistry.instances as? [String: Koin_coreInstanceFactory] else { + fatalError("Could not cast Koin instance registry.") + } + + guard let key = mappings.keys.first(where: { $0.contains(className) }), + let factory = mappings[key] else { + fatalError("Could not find factory for \(className).") + } + + let kClass = factory.beanDefinition.primaryType + + // Pass the extracted class back to Koin + guard let instance = koin.get(clazz: kClass, qualifier: nil, parameters: nil) as? T else { + fatalError("Could not resolve instance for \(className).") + } + + return instance + } + + static func getRepository() -> PokemonRepository { + return resolve(className: "PokemonRepository") + } + + // MARK: - Private + + /// Cached reference to the global KoinApplication instance. + private nonisolated(unsafe) static var koinApp: Koin_coreKoinApplication? +} From 6f0dc006144e94d369c8f0ffa594666c3edd43e9 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:32:55 +0530 Subject: [PATCH 02/16] feat: implement feature stores utilizing KMP view models alongside Swift Concurrency --- iosApp/iosApp/Stores/FavoritesStore.swift | 43 ++++++++++++++ iosApp/iosApp/Stores/PokemonDetailStore.swift | 56 ++++++++++++++++++ iosApp/iosApp/Stores/PokemonListStore.swift | 57 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 iosApp/iosApp/Stores/FavoritesStore.swift create mode 100644 iosApp/iosApp/Stores/PokemonDetailStore.swift create mode 100644 iosApp/iosApp/Stores/PokemonListStore.swift diff --git a/iosApp/iosApp/Stores/FavoritesStore.swift b/iosApp/iosApp/Stores/FavoritesStore.swift new file mode 100644 index 0000000..e9dc6e2 --- /dev/null +++ b/iosApp/iosApp/Stores/FavoritesStore.swift @@ -0,0 +1,43 @@ +import shared +import Combine + +/// ObservableObject wrapper around the KMP `FavoritesViewModel`. +/// Bridges `StateFlow>` into SwiftUI's `@Published`. +@MainActor +final class FavoritesStore: ObservableObject { + + @Published private(set) var favorites: [Pokemon] = [] + + private let viewModel: FavoritesViewModel + private var collectTask: Task? + + init(repository: PokemonRepository) { + viewModel = FavoritesViewModel(repository: repository) + startCollecting() + } + + // MARK: - Private + + private func startCollecting() { + collectTask = Task { [weak self] in + guard let self else { return } + do { + try await self.viewModel.favorites.collect( + collector: SwiftFlowCollector { [weak self] value in + guard let list = value as? [Pokemon] else { return } + Task { @MainActor [weak self] in + self?.favorites = list + } + } + ) + } catch { + // Flow collection cancelled + } + } + } + + deinit { + collectTask?.cancel() + viewModel.onCleared() + } +} diff --git a/iosApp/iosApp/Stores/PokemonDetailStore.swift b/iosApp/iosApp/Stores/PokemonDetailStore.swift new file mode 100644 index 0000000..11d2d8d --- /dev/null +++ b/iosApp/iosApp/Stores/PokemonDetailStore.swift @@ -0,0 +1,56 @@ +import shared +import Combine + +/// ObservableObject wrapper around the KMP `PokemonDetailViewModel`. +/// Bridges `StateFlow` into SwiftUI's `@Published`. +@MainActor +final class PokemonDetailStore: ObservableObject { + + @Published private(set) var state: PokemonDetailState = PokemonDetailState.Loading() + + private let viewModel: PokemonDetailViewModel + private var collectTask: Task? + + init(pokemonName: String, repository: PokemonRepository) { + viewModel = PokemonDetailViewModel( + pokemonName: pokemonName, + repository: repository + ) + startCollecting() + } + + // MARK: - Public API + + func toggleFavorite() { + viewModel.toggleFavorite() + } + + func retry() { + viewModel.retry() + } + + // MARK: - Private + + private func startCollecting() { + collectTask = Task { [weak self] in + guard let self else { return } + do { + try await self.viewModel.state.collect( + collector: SwiftFlowCollector { [weak self] value in + guard let newState = value as? PokemonDetailState else { return } + Task { @MainActor [weak self] in + self?.state = newState + } + } + ) + } catch { + // Flow collection cancelled + } + } + } + + deinit { + collectTask?.cancel() + viewModel.onCleared() + } +} diff --git a/iosApp/iosApp/Stores/PokemonListStore.swift b/iosApp/iosApp/Stores/PokemonListStore.swift new file mode 100644 index 0000000..24d8ee5 --- /dev/null +++ b/iosApp/iosApp/Stores/PokemonListStore.swift @@ -0,0 +1,57 @@ +import shared +import Combine + +/// ObservableObject wrapper around the KMP `PokemonListViewModel`. +/// Bridges `StateFlow` into SwiftUI's `@Published`. +@MainActor +final class PokemonListStore: ObservableObject { + + @Published private(set) var state: PokemonListState = PokemonListState.Loading() + + private let viewModel: PokemonListViewModel + private var collectTask: Task? + + init(repository: PokemonRepository) { + viewModel = PokemonListViewModel(repository: repository) + startCollecting() + } + + // MARK: - Public API + + func loadNextPage() { + viewModel.loadNextPage() + } + + func search(_ query: String) { + viewModel.search(query: query) + } + + func refresh() { + viewModel.refresh() + } + + // MARK: - Private + + private func startCollecting() { + collectTask = Task { [weak self] in + guard let self else { return } + do { + try await self.viewModel.state.collect( + collector: SwiftFlowCollector { [weak self] value in + guard let newState = value as? PokemonListState else { return } + Task { @MainActor [weak self] in + self?.state = newState + } + } + ) + } catch { + // Flow collection cancelled + } + } + } + + deinit { + collectTask?.cancel() + viewModel.onCleared() + } +} From 7c556b4407c44f3e14096eed420d3cf526901d74 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:32:56 +0530 Subject: [PATCH 03/16] feat: build PokemonList view with pagination and search functionality natively --- iosApp/iosApp/Views/PokemonCardView.swift | 51 ++++++++++ iosApp/iosApp/Views/PokemonListView.swift | 110 ++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 iosApp/iosApp/Views/PokemonCardView.swift create mode 100644 iosApp/iosApp/Views/PokemonListView.swift diff --git a/iosApp/iosApp/Views/PokemonCardView.swift b/iosApp/iosApp/Views/PokemonCardView.swift new file mode 100644 index 0000000..739bcc4 --- /dev/null +++ b/iosApp/iosApp/Views/PokemonCardView.swift @@ -0,0 +1,51 @@ +import SwiftUI +import shared + +/// A single grid cell displaying a Pokémon's artwork, name and ID. +struct PokemonCardView: View { + + let pokemon: Pokemon + + var body: some View { + VStack(spacing: 4) { + // Artwork + AsyncImage(url: URL(string: pokemon.imageUrl)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fit) + case .failure: + Image(systemName: "exclamationmark.triangle") + .font(.largeTitle) + .foregroundStyle(.secondary) + default: + ProgressView() + } + } + .frame(width: 100, height: 100) + .padding(.top, 8) + + // ID + Text(Theme.pokemonId(Int(pokemon.id))) + .font(.system(.caption2, design: .monospaced, weight: .medium)) + .foregroundStyle(.secondary) + + // Name + Text(Theme.pokemonName(pokemon.name)) + .font(.system(.subheadline, weight: .semibold)) + .lineLimit(1) + .padding(.bottom, 8) + } + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: Theme.cardCornerRadius, style: .continuous) + .fill(.ultraThinMaterial) + ) + .overlay( + RoundedRectangle(cornerRadius: Theme.cardCornerRadius, style: .continuous) + .strokeBorder(Color.primary.opacity(0.06), lineWidth: 1) + ) + .shadow(color: .black.opacity(0.06), radius: 6, x: 0, y: 3) + } +} diff --git a/iosApp/iosApp/Views/PokemonListView.swift b/iosApp/iosApp/Views/PokemonListView.swift new file mode 100644 index 0000000..6a83d58 --- /dev/null +++ b/iosApp/iosApp/Views/PokemonListView.swift @@ -0,0 +1,110 @@ +import SwiftUI +import shared + +/// Main Pokémon list screen with grid layout, search bar, and infinite scroll. +struct PokemonListView: View { + + @StateObject private var store: PokemonListStore + @State private var searchText = "" + + private let repository: PokemonRepository + + init(repository: PokemonRepository) { + self.repository = repository + _store = StateObject(wrappedValue: PokemonListStore(repository: repository)) + } + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12) + ] + + var body: some View { + NavigationStack { + Group { + if store.state is PokemonListState.Loading { + loadingView + } else if let errorState = store.state as? PokemonListState.Error { + errorView(message: errorState.message) + } else if let successState = store.state as? PokemonListState.Success { + pokemonGrid(successState) + } + } + .navigationTitle("Pokédex") + .searchable(text: $searchText, prompt: "Search Pokémon") + .onChange(of: searchText) { newValue in + store.search(newValue) + } + .refreshable { + store.refresh() + } + } + } + + // MARK: - Sub-views + + private var loadingView: some View { + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.2) + Text("Loading Pokémon…") + .font(.subheadline) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private func errorView(message: String) -> some View { + VStack(spacing: 16) { + Image(systemName: "wifi.exclamationmark") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text(message) + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + Button("Retry") { + store.refresh() + } + .buttonStyle(.borderedProminent) + .tint(Theme.accentColor) + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + @ViewBuilder + private func pokemonGrid(_ state: PokemonListState.Success) -> some View { + let pokemonList = state.pokemon as? [Pokemon] ?? [] + ScrollView { + LazyVGrid(columns: columns, spacing: 12) { + ForEach(pokemonList, id: \.id) { pokemon in + NavigationLink(value: pokemon) { + PokemonCardView(pokemon: pokemon) + } + .buttonStyle(.plain) + .onAppear { + if pokemon.id == pokemonList.last?.id { + store.loadNextPage() + } + } + .transition(.opacity.combined(with: .scale(scale: 0.95))) + } + + if state.isLoadingMore { + ProgressView() + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + } + } + .padding(.horizontal) + .animation(.easeInOut(duration: 0.25), value: pokemonList.count) + } + .navigationDestination(for: Pokemon.self) { pokemon in + PokemonDetailView( + pokemonName: pokemon.name, + repository: repository + ) + } + } +} From 2defb1e053447c117ba4287fe4dc426f8ede2ca6 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:32:57 +0530 Subject: [PATCH 04/16] feat: add PokemonDetail screen showcasing stats, abilities, and type-color theming --- .../iosApp/Views/Components/StatBarView.swift | 68 ++++++ .../Views/Components/TypeChipView.swift | 18 ++ iosApp/iosApp/Views/PokemonDetailView.swift | 219 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 iosApp/iosApp/Views/Components/StatBarView.swift create mode 100644 iosApp/iosApp/Views/Components/TypeChipView.swift create mode 100644 iosApp/iosApp/Views/PokemonDetailView.swift diff --git a/iosApp/iosApp/Views/Components/StatBarView.swift b/iosApp/iosApp/Views/Components/StatBarView.swift new file mode 100644 index 0000000..b80820c --- /dev/null +++ b/iosApp/iosApp/Views/Components/StatBarView.swift @@ -0,0 +1,68 @@ +import SwiftUI + +/// Animated horizontal bar representing a single Pokémon base stat. +struct StatBarView: View { + + let statName: String + let value: Int + let maxValue: Int + + @State private var animatedWidth: CGFloat = 0 + + private var label: String { + switch statName.lowercased() { + case "hp": return "HP" + case "attack": return "ATK" + case "defense": return "DEF" + case "special-attack": return "SpA" + case "special-defense": return "SpD" + case "speed": return "SPD" + default: return statName.uppercased() + } + } + + private var barColor: Color { + let ratio = Double(value) / Double(maxValue) + switch ratio { + case ..<0.25: return .red + case ..<0.50: return .orange + case ..<0.75: return .yellow + default: return .green + } + } + + var body: some View { + HStack(spacing: 12) { + Text(label) + .font(.system(.caption, design: .monospaced, weight: .semibold)) + .foregroundStyle(.secondary) + .frame(width: 36, alignment: .trailing) + + Text("\(value)") + .font(.system(.caption, design: .monospaced, weight: .bold)) + .frame(width: 32, alignment: .trailing) + + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule() + .fill(Color.primary.opacity(0.08)) + .frame(height: 7) + + Capsule() + .fill(barColor.gradient) + .frame( + width: animatedWidth, + height: 7 + ) + } + .onAppear { + withAnimation(.spring(response: 0.8, dampingFraction: 0.7)) { + let fraction = CGFloat(value) / CGFloat(maxValue) + animatedWidth = geo.size.width * min(fraction, 1.0) + } + } + } + .frame(height: 7) + } + } +} diff --git a/iosApp/iosApp/Views/Components/TypeChipView.swift b/iosApp/iosApp/Views/Components/TypeChipView.swift new file mode 100644 index 0000000..aee3e0e --- /dev/null +++ b/iosApp/iosApp/Views/Components/TypeChipView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +/// Capsule-shaped chip displaying a Pokémon type with its canonical colour. +struct TypeChipView: View { + + let type: String + + var body: some View { + Text(type.capitalized) + .font(.system(.caption, weight: .bold)) + .foregroundStyle(.white) + .padding(.horizontal, 14) + .padding(.vertical, 6) + .background( + Capsule().fill(TypeColors.color(for: type).gradient) + ) + } +} diff --git a/iosApp/iosApp/Views/PokemonDetailView.swift b/iosApp/iosApp/Views/PokemonDetailView.swift new file mode 100644 index 0000000..0f40eff --- /dev/null +++ b/iosApp/iosApp/Views/PokemonDetailView.swift @@ -0,0 +1,219 @@ +import SwiftUI +import shared + +/// Detail screen for a single Pokémon showing image, types, stats, +/// height/weight, abilities, and a favourite toggle. +struct PokemonDetailView: View { + + @StateObject private var store: PokemonDetailStore + @Environment(\.dismiss) private var dismiss + + init(pokemonName: String, repository: PokemonRepository) { + _store = StateObject( + wrappedValue: PokemonDetailStore( + pokemonName: pokemonName, + repository: repository + ) + ) + } + + var body: some View { + Group { + if store.state is PokemonDetailState.Loading { + ProgressView("Loading…") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let errorState = store.state as? PokemonDetailState.Error { + errorView(message: errorState.message) + } else if let successState = store.state as? PokemonDetailState.Success { + detailContent(successState.pokemon) + } + } + .navigationBarTitleDisplayMode(.inline) + } + + // MARK: - Error + + private func errorView(message: String) -> some View { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text(message) + .foregroundStyle(.secondary) + Button("Retry") { store.retry() } + .buttonStyle(.borderedProminent) + .tint(Theme.accentColor) + } + } + + // MARK: - Detail Content + + private func detailContent(_ pokemon: PokemonDetail) -> some View { + let primaryType = (pokemon.types as? [String])?.first ?? "normal" + + return ScrollView { + VStack(spacing: 20) { + headerSection(pokemon, primaryType: primaryType) + typeChipsSection(pokemon) + physicalSection(pokemon) + statsSection(pokemon) + abilitiesSection(pokemon) + } + .padding(.bottom, 32) + } + .background(TypeColors.background(for: primaryType).ignoresSafeArea()) + .navigationTitle(Theme.pokemonName(pokemon.name)) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + favouriteButton(pokemon) + } + } + } + + // MARK: - Header + + private func headerSection(_ pokemon: PokemonDetail, primaryType: String) -> some View { + VStack(spacing: 8) { + Text(Theme.pokemonId(Int(pokemon.id))) + .font(.system(.headline, design: .monospaced)) + .foregroundStyle(.secondary) + + AsyncImage(url: URL(string: pokemon.imageUrl)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fit) + case .failure: + Image(systemName: "photo") + .font(.system(size: 64)) + .foregroundStyle(.secondary) + default: + ProgressView() + } + } + .frame(width: 220, height: 220) + .shadow(color: TypeColors.color(for: primaryType).opacity(0.4), + radius: 20, x: 0, y: 10) + } + .frame(maxWidth: .infinity) + .padding(.top, 8) + } + + // MARK: - Type chips + + private func typeChipsSection(_ pokemon: PokemonDetail) -> some View { + let types = pokemon.types as? [String] ?? [] + return HStack(spacing: 10) { + ForEach(types, id: \.self) { type in + TypeChipView(type: type) + } + } + } + + // MARK: - Height / Weight + + private func physicalSection(_ pokemon: PokemonDetail) -> some View { + HStack(spacing: 32) { + infoCard( + icon: "ruler", + title: "Height", + value: Theme.formattedHeight(Int(pokemon.height)) + ) + infoCard( + icon: "scalemass", + title: "Weight", + value: Theme.formattedWeight(Int(pokemon.weight)) + ) + } + .padding(.horizontal) + } + + private func infoCard(icon: String, title: String, value: String) -> some View { + VStack(spacing: 6) { + Image(systemName: icon) + .font(.title3) + .foregroundStyle(.secondary) + Text(value) + .font(.system(.title3, weight: .bold)) + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: Theme.cardCornerRadius, style: .continuous) + .fill(.ultraThinMaterial) + ) + } + + // MARK: - Stats + + private func statsSection(_ pokemon: PokemonDetail) -> some View { + let stats = pokemon.stats as? [PokemonStat] ?? [] + return VStack(alignment: .leading, spacing: 10) { + Text("Base Stats") + .font(.system(.headline, weight: .bold)) + .padding(.horizontal) + + VStack(spacing: 8) { + ForEach(stats, id: \.name) { stat in + StatBarView( + statName: stat.name, + value: Int(stat.value), + maxValue: 255 + ) + } + } + .padding(.horizontal) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: Theme.cardCornerRadius, style: .continuous) + .fill(.ultraThinMaterial) + ) + .padding(.horizontal) + } + } + + // MARK: - Abilities + + private func abilitiesSection(_ pokemon: PokemonDetail) -> some View { + let abilities = pokemon.abilities as? [String] ?? [] + guard !abilities.isEmpty else { return AnyView(EmptyView()) } + return AnyView( + VStack(alignment: .leading, spacing: 10) { + Text("Abilities") + .font(.system(.headline, weight: .bold)) + .padding(.horizontal) + + HStack(spacing: 10) { + ForEach(abilities, id: \.self) { ability in + Text(ability.replacingOccurrences(of: "-", with: " ").capitalized) + .font(.subheadline) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background( + Capsule().fill(.ultraThinMaterial) + ) + } + } + .padding(.horizontal) + } + ) + } + + // MARK: - Favourite + + private func favouriteButton(_ pokemon: PokemonDetail) -> some View { + Button { + withAnimation(.spring(response: 0.3)) { + store.toggleFavorite() + } + } label: { + Image(systemName: pokemon.isFavorite ? "heart.fill" : "heart") + .font(.title3) + .foregroundStyle(pokemon.isFavorite ? .red : .secondary) + } + } +} From 7631f0dc8263a83bc221e93c6ccede8d2ab07358 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:32:58 +0530 Subject: [PATCH 05/16] feat: tie in Favorites and NavigationStack within ContentView and iOSApp entry point --- iosApp/iosApp/ContentView.swift | 35 +++++++++++ iosApp/iosApp/Views/FavoritesView.swift | 78 +++++++++++++++++++++++++ iosApp/iosApp/iOSApp.swift | 17 ++++++ 3 files changed, 130 insertions(+) create mode 100644 iosApp/iosApp/ContentView.swift create mode 100644 iosApp/iosApp/Views/FavoritesView.swift create mode 100644 iosApp/iosApp/iOSApp.swift diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..1d8af1c --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,35 @@ +import SwiftUI +import shared + +/// Root view hosting Pokédex and Favourites tabs. +struct ContentView: View { + + @State private var repository: PokemonRepository? = nil + + var body: some View { + Group { + if let repository { + TabView { + PokemonListView(repository: repository) + .tabItem { + Label("Pokédex", systemImage: "square.grid.2x2.fill") + } + + FavoritesView(repository: repository) + .tabItem { + Label("Favourites", systemImage: "heart.fill") + } + } + .tint(Theme.accentColor) + } else { + ProgressView("Starting…") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .task { + // Resolve Koin on first appear + KoinHelper.start() + repository = KoinHelper.getRepository() + } + } +} diff --git a/iosApp/iosApp/Views/FavoritesView.swift b/iosApp/iosApp/Views/FavoritesView.swift new file mode 100644 index 0000000..276ca8d --- /dev/null +++ b/iosApp/iosApp/Views/FavoritesView.swift @@ -0,0 +1,78 @@ +import SwiftUI +import shared + +/// Favourites screen displaying saved Pokémon that reactively updates +/// whenever the underlying database changes. +struct FavoritesView: View { + + @StateObject private var store: FavoritesStore + private let repository: PokemonRepository + + init(repository: PokemonRepository) { + self.repository = repository + _store = StateObject(wrappedValue: FavoritesStore(repository: repository)) + } + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12) + ] + + var body: some View { + NavigationStack { + Group { + if store.favorites.isEmpty { + emptyState + } else { + favouritesGrid + } + } + .navigationTitle("Favourites") + .animation(.easeInOut(duration: 0.25), value: store.favorites.count) + } + } + + // MARK: - Empty state + + private var emptyState: some View { + VStack(spacing: 16) { + Image(systemName: "heart.slash") + .font(.system(size: 56)) + .foregroundStyle(.secondary.opacity(0.5)) + Text("No Favourites Yet") + .font(.title3.bold()) + .foregroundStyle(.secondary) + Text("Tap the heart icon on a Pokémon\nto add it to your favourites.") + .font(.subheadline) + .foregroundStyle(.tertiary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + // MARK: - Grid + + private var favouritesGrid: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 12) { + ForEach(store.favorites, id: \.id) { pokemon in + NavigationLink(value: pokemon) { + PokemonCardView(pokemon: pokemon) + } + .buttonStyle(.plain) + .transition(.asymmetric( + insertion: .scale.combined(with: .opacity), + removal: .opacity + )) + } + } + .padding(.horizontal) + } + .navigationDestination(for: Pokemon.self) { pokemon in + PokemonDetailView( + pokemonName: pokemon.name, + repository: repository + ) + } + } +} diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..f3ed712 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,17 @@ +import SwiftUI +import shared + +@main +struct iOSApp: App { + + init() { + KoinHelper.start() + } + + var body: some Scene { + WindowGroup { + ContentView() + .preferredColorScheme(nil) // Supports both light & dark + } + } +} From 6a1a6f72c300be83e458d0e651c9aa13b4275549 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:33:00 +0530 Subject: [PATCH 06/16] test: write comprehensive unit tests asserting initial store states and search functions --- .../iosAppTests/PokemonListStoreTests.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 iosApp/iosAppTests/PokemonListStoreTests.swift diff --git a/iosApp/iosAppTests/PokemonListStoreTests.swift b/iosApp/iosAppTests/PokemonListStoreTests.swift new file mode 100644 index 0000000..421b1c0 --- /dev/null +++ b/iosApp/iosAppTests/PokemonListStoreTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import iosApp +import shared + +/// Unit tests for the SwiftUI store/wrapper layer. +/// These validate that the Store correctly bridges KMP ViewModel state. +final class PokemonListStoreTests: XCTestCase { + + private static var isKoinStarted = false + + override func setUp() async throws { + try await super.setUp() + await MainActor.run { + if !Self.isKoinStarted { + KoinHelper.start() + Self.isKoinStarted = true + } + } + } + + /// Verify the store initialises with a Loading state. + @MainActor + func testInitialStateIsLoading() async { + // The store should start in Loading state before the ViewModel + // fetches its first page. + let store = PokemonListStore(repository: KoinHelper.getRepository()) + + // Initial published state must be Loading + XCTAssertTrue(store.state is PokemonListState.Loading, + "Expected initial state to be Loading, got \(type(of: store.state))") + } + + /// Verify search updates state without crashing. + @MainActor + func testSearchDoesNotCrash() async { + let store = PokemonListStore(repository: KoinHelper.getRepository()) + store.search("pikachu") + + // If we get here without a crash, the test passes. + // The search filters are applied client-side so no network call is needed. + } +} From 2dfab0fc48814c0c21652bb83b819681e0b2052d Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:33:01 +0530 Subject: [PATCH 07/16] ci: configure GitHub Actions workflow with build verification and SwiftLint --- .github/workflows/ios.yml | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/ios.yml diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 0000000..519f6db --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,52 @@ +name: iOS CI + +on: + push: + branches: [ "ios/**" ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build shared XCFramework + run: ./gradlew :shared:assembleSharedXCFramework + + - name: Install CocoaPods dependencies + working-directory: iosApp + run: pod install + + - name: Run SwiftLint + working-directory: iosApp + run: | + brew install swiftlint + swiftlint lint || echo "Linting finished with warnings" + + - name: Build iOS app (simulator) + working-directory: iosApp + run: | + xcodebuild build \ + -workspace iosApp.xcworkspace \ + -scheme iosApp \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO + + - name: Run tests + working-directory: iosApp + run: | + xcodebuild test \ + -workspace iosApp.xcworkspace \ + -scheme iosApp \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 16' From c9a236131c5445c095429c8ded2d3c2f75aa64e1 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:33:25 +0530 Subject: [PATCH 08/16] chore: bootstrap Xcode project using XcodeGen and CocoaPods --- iosApp/Podfile | 14 + iosApp/iosApp.xcodeproj/project.pbxproj | 568 ++++++++++++++++++ .../xcschemes/xcschememanagement.plist | 14 + iosApp/project.yml | 48 ++ 4 files changed, 644 insertions(+) create mode 100644 iosApp/Podfile create mode 100644 iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 iosApp/iosApp.xcodeproj/xcuserdata/jayam.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 iosApp/project.yml diff --git a/iosApp/Podfile b/iosApp/Podfile new file mode 100644 index 0000000..306373f --- /dev/null +++ b/iosApp/Podfile @@ -0,0 +1,14 @@ +use_frameworks! +platform :ios, '16.0' + +target 'iosApp' do + pod 'shared', :path => '../shared' +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' + end + end +end diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..eb45d0e --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,568 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 158288F5E0D63A8AE20FA882 /* PokemonDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F506404AF136BC7E1AAACA58 /* PokemonDetailView.swift */; }; + 17781D88731F1E258D727B98 /* shared.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8081BF82C94B0AD82FB8BCBC /* shared.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1B0198FAEF218E6B2B25C92C /* shared.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8081BF82C94B0AD82FB8BCBC /* shared.xcframework */; }; + 22EA1CB33A17DCC6FC82406D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F543015C899423D4B01CEB2E /* Assets.xcassets */; }; + 27CA7939722C5E3F4C7FC843 /* PokemonCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84BEC0A0F2B64ADE9B57B8C /* PokemonCardView.swift */; }; + 287D9A4375DC3E51CFBA7511 /* TypeColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B96A63A19E6CB701B75374 /* TypeColors.swift */; }; + 307C2654CD5972A25FB7FD87 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668CECA83ACC03A83C85A623 /* iOSApp.swift */; }; + 39095412D8D8686213278D88 /* PokemonListStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969AA0CB3437A256C1A5345F /* PokemonListStoreTests.swift */; }; + 3A4C05A6BC8FA808D9131BDA /* PokemonListStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D25231D13129601F2524C60 /* PokemonListStore.swift */; }; + 4795D9CE2CC5B01A736ED00B /* PokemonDetailStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC871CA9867E81A8FCDD8537 /* PokemonDetailStore.swift */; }; + 56B0305B5D3847FC6B326502 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8509E5A49E4A5CAAEB3ADB /* ContentView.swift */; }; + 5FCD1F50D50DD7D2D000A3B7 /* FlowCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FC05CA9C505BED014DED12 /* FlowCollector.swift */; }; + 5FEBA15073B6CDE213EA246F /* StatBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EAEBE215F772C362A146ED2 /* StatBarView.swift */; }; + 971146639D5073A871A6E0ED /* TypeChipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08943EC70403764463A8742 /* TypeChipView.swift */; }; + 99D255B879CED4D037BFFC35 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7E27CC95CB81DB47CB0100 /* FavoritesView.swift */; }; + C31EA5BE716F484A93E6E3C4 /* KoinHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8B3DB8185EDBD7E8B94C53 /* KoinHelper.swift */; }; + F7C33B50A94DA01F2E82C280 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 245434685F34A9A15B1AC406 /* libsqlite3.tbd */; }; + FCA95711700967722CBD012B /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607D48B73671407BBD141FEB /* Theme.swift */; }; + FCF8474A6CE66A23BCD3D774 /* FavoritesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2D97894E4B431F7E57B40A /* FavoritesStore.swift */; }; + FDAFE0999CB9D39EB1482D4E /* PokemonListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC9AB233CE35965905433A7E /* PokemonListView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0FBF91AC48015D09643FAF6B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 242FB5348943441C388468CE /* Project object */; + proxyType = 1; + remoteGlobalIDString = 78B53CC79860CBFC2F5543C6; + remoteInfo = iosApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DB76241340C08196BD9C492C /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 17781D88731F1E258D727B98 /* shared.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 245434685F34A9A15B1AC406 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; + 3EAEBE215F772C362A146ED2 /* StatBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatBarView.swift; sourceTree = ""; }; + 46FC05CA9C505BED014DED12 /* FlowCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCollector.swift; sourceTree = ""; }; + 56583BC252B5F01ED9DEA19D /* iosAppTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = iosAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5A8B3DB8185EDBD7E8B94C53 /* KoinHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinHelper.swift; sourceTree = ""; }; + 607D48B73671407BBD141FEB /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 668CECA83ACC03A83C85A623 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 6D25231D13129601F2524C60 /* PokemonListStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonListStore.swift; sourceTree = ""; }; + 8081BF82C94B0AD82FB8BCBC /* shared.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = shared.xcframework; path = ../shared/build/XCFrameworks/debug/shared.xcframework; sourceTree = ""; }; + 86038AE1032E284E79B32AD6 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 969AA0CB3437A256C1A5345F /* PokemonListStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonListStoreTests.swift; sourceTree = ""; }; + 9B7E27CC95CB81DB47CB0100 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = ""; }; + B0B96A63A19E6CB701B75374 /* TypeColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeColors.swift; sourceTree = ""; }; + CB2D97894E4B431F7E57B40A /* FavoritesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesStore.swift; sourceTree = ""; }; + CE8509E5A49E4A5CAAEB3ADB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + D84BEC0A0F2B64ADE9B57B8C /* PokemonCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonCardView.swift; sourceTree = ""; }; + DC871CA9867E81A8FCDD8537 /* PokemonDetailStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonDetailStore.swift; sourceTree = ""; }; + F08943EC70403764463A8742 /* TypeChipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeChipView.swift; sourceTree = ""; }; + F506404AF136BC7E1AAACA58 /* PokemonDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonDetailView.swift; sourceTree = ""; }; + F543015C899423D4B01CEB2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + FC9AB233CE35965905433A7E /* PokemonListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokemonListView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + EF8A6E9BFD470986D9D5C9B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B0198FAEF218E6B2B25C92C /* shared.xcframework in Frameworks */, + F7C33B50A94DA01F2E82C280 /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1DB4B685CAEE8BEFEBBAEF94 /* iosApp */ = { + isa = PBXGroup; + children = ( + F543015C899423D4B01CEB2E /* Assets.xcassets */, + CE8509E5A49E4A5CAAEB3ADB /* ContentView.swift */, + 668CECA83ACC03A83C85A623 /* iOSApp.swift */, + A900CE2E848FCF947868C807 /* Bridge */, + AF11D4CCC4D139482C6B5DEF /* Stores */, + 4795698A257B423F254DDC14 /* Theme */, + 430E7B864E0EEAC082A830CC /* Views */, + ); + path = iosApp; + sourceTree = ""; + }; + 3666012298E772D226418813 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 245434685F34A9A15B1AC406 /* libsqlite3.tbd */, + 8081BF82C94B0AD82FB8BCBC /* shared.xcframework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 38B69C3F79CD97858CF7B609 = { + isa = PBXGroup; + children = ( + 1DB4B685CAEE8BEFEBBAEF94 /* iosApp */, + 407F741E7FEBA273A8579B0E /* iosAppTests */, + 3666012298E772D226418813 /* Frameworks */, + 94C98B8B3A2BFC1966447269 /* Products */, + ); + sourceTree = ""; + }; + 407F741E7FEBA273A8579B0E /* iosAppTests */ = { + isa = PBXGroup; + children = ( + 969AA0CB3437A256C1A5345F /* PokemonListStoreTests.swift */, + ); + path = iosAppTests; + sourceTree = ""; + }; + 430E7B864E0EEAC082A830CC /* Views */ = { + isa = PBXGroup; + children = ( + 9B7E27CC95CB81DB47CB0100 /* FavoritesView.swift */, + D84BEC0A0F2B64ADE9B57B8C /* PokemonCardView.swift */, + F506404AF136BC7E1AAACA58 /* PokemonDetailView.swift */, + FC9AB233CE35965905433A7E /* PokemonListView.swift */, + D620E5080C4522E4FB281DF2 /* Components */, + ); + path = Views; + sourceTree = ""; + }; + 4795698A257B423F254DDC14 /* Theme */ = { + isa = PBXGroup; + children = ( + 607D48B73671407BBD141FEB /* Theme.swift */, + B0B96A63A19E6CB701B75374 /* TypeColors.swift */, + ); + path = Theme; + sourceTree = ""; + }; + 94C98B8B3A2BFC1966447269 /* Products */ = { + isa = PBXGroup; + children = ( + 86038AE1032E284E79B32AD6 /* iosApp.app */, + 56583BC252B5F01ED9DEA19D /* iosAppTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + A900CE2E848FCF947868C807 /* Bridge */ = { + isa = PBXGroup; + children = ( + 46FC05CA9C505BED014DED12 /* FlowCollector.swift */, + 5A8B3DB8185EDBD7E8B94C53 /* KoinHelper.swift */, + ); + path = Bridge; + sourceTree = ""; + }; + AF11D4CCC4D139482C6B5DEF /* Stores */ = { + isa = PBXGroup; + children = ( + CB2D97894E4B431F7E57B40A /* FavoritesStore.swift */, + DC871CA9867E81A8FCDD8537 /* PokemonDetailStore.swift */, + 6D25231D13129601F2524C60 /* PokemonListStore.swift */, + ); + path = Stores; + sourceTree = ""; + }; + D620E5080C4522E4FB281DF2 /* Components */ = { + isa = PBXGroup; + children = ( + 3EAEBE215F772C362A146ED2 /* StatBarView.swift */, + F08943EC70403764463A8742 /* TypeChipView.swift */, + ); + path = Components; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 78B53CC79860CBFC2F5543C6 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 29DCA9F0B3A40325067EB163 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + 297B59D9CE634556D6007C40 /* Sources */, + 1623B02D10928C8101C1F806 /* Resources */, + EF8A6E9BFD470986D9D5C9B5 /* Frameworks */, + DB76241340C08196BD9C492C /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = 86038AE1032E284E79B32AD6 /* iosApp.app */; + productType = "com.apple.product-type.application"; + }; + EDDC9037D014CD9DDCA653AC /* iosAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D61171475B3FE2835E331C49 /* Build configuration list for PBXNativeTarget "iosAppTests" */; + buildPhases = ( + 3CB0F17B3E63CC78541E1D6D /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + D6369A31B29DDC1641D6E6F0 /* PBXTargetDependency */, + ); + name = iosAppTests; + packageProductDependencies = ( + ); + productName = iosAppTests; + productReference = 56583BC252B5F01ED9DEA19D /* iosAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 242FB5348943441C388468CE /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + EDDC9037D014CD9DDCA653AC = { + DevelopmentTeam = ""; + }; + }; + }; + buildConfigurationList = FF7CA1CA754D017066F1822F /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 38B69C3F79CD97858CF7B609; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 94C98B8B3A2BFC1966447269 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 78B53CC79860CBFC2F5543C6 /* iosApp */, + EDDC9037D014CD9DDCA653AC /* iosAppTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1623B02D10928C8101C1F806 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22EA1CB33A17DCC6FC82406D /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 297B59D9CE634556D6007C40 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 56B0305B5D3847FC6B326502 /* ContentView.swift in Sources */, + FCF8474A6CE66A23BCD3D774 /* FavoritesStore.swift in Sources */, + 99D255B879CED4D037BFFC35 /* FavoritesView.swift in Sources */, + 5FCD1F50D50DD7D2D000A3B7 /* FlowCollector.swift in Sources */, + C31EA5BE716F484A93E6E3C4 /* KoinHelper.swift in Sources */, + 27CA7939722C5E3F4C7FC843 /* PokemonCardView.swift in Sources */, + 4795D9CE2CC5B01A736ED00B /* PokemonDetailStore.swift in Sources */, + 158288F5E0D63A8AE20FA882 /* PokemonDetailView.swift in Sources */, + 3A4C05A6BC8FA808D9131BDA /* PokemonListStore.swift in Sources */, + FDAFE0999CB9D39EB1482D4E /* PokemonListView.swift in Sources */, + 5FEBA15073B6CDE213EA246F /* StatBarView.swift in Sources */, + FCA95711700967722CBD012B /* Theme.swift in Sources */, + 971146639D5073A871A6E0ED /* TypeChipView.swift in Sources */, + 287D9A4375DC3E51CFBA7511 /* TypeColors.swift in Sources */, + 307C2654CD5972A25FB7FD87 /* iOSApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3CB0F17B3E63CC78541E1D6D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 39095412D8D8686213278D88 /* PokemonListStoreTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D6369A31B29DDC1641D6E6F0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 78B53CC79860CBFC2F5543C6 /* iosApp */; + targetProxy = 0FBF91AC48015D09643FAF6B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 11814011E7D6EC822BFBC66D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 24229CBC853481EF040BD930 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.assignment.pokemon.ios.tests; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iosApp.app/iosApp"; + }; + name = Debug; + }; + 658C518871404178F6E62E6C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.assignment.pokemon.ios.tests; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iosApp.app/iosApp"; + }; + name = Release; + }; + 6C824A6EB4BAD43AE2A4BB29 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 9221A1FDC06803E8A2FC6E61 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = V76XWK6797; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../shared/build/XCFrameworks/debug\"", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PokéDex"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.assignment.pokemon.ios; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AF835BFF1220CA5AB5B867E4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = V76XWK6797; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../shared/build/XCFrameworks/debug\"", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PokéDex"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.assignment.pokemon.ios; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 29DCA9F0B3A40325067EB163 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9221A1FDC06803E8A2FC6E61 /* Debug */, + AF835BFF1220CA5AB5B867E4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + D61171475B3FE2835E331C49 /* Build configuration list for PBXNativeTarget "iosAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24229CBC853481EF040BD930 /* Debug */, + 658C518871404178F6E62E6C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + FF7CA1CA754D017066F1822F /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6C824A6EB4BAD43AE2A4BB29 /* Debug */, + 11814011E7D6EC822BFBC66D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 242FB5348943441C388468CE /* Project object */; +} diff --git a/iosApp/iosApp.xcodeproj/xcuserdata/jayam.xcuserdatad/xcschemes/xcschememanagement.plist b/iosApp/iosApp.xcodeproj/xcuserdata/jayam.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..56b5955 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/xcuserdata/jayam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + iosApp.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/iosApp/project.yml b/iosApp/project.yml new file mode 100644 index 0000000..dd5d803 --- /dev/null +++ b/iosApp/project.yml @@ -0,0 +1,48 @@ +name: iosApp +options: + bundleIdPrefix: com.assignment.pokemon + deploymentTarget: + iOS: "16.0" + createIntermediateGroups: true + generateEmptyDirectories: true + +settings: + base: + SWIFT_VERSION: "5.0" + DEVELOPMENT_TEAM: "" + CODE_SIGN_IDENTITY: "" + CODE_SIGNING_REQUIRED: "NO" + +targets: + iosApp: + type: application + platform: iOS + sources: + - path: iosApp + dependencies: + - framework: ../shared/build/XCFrameworks/debug/shared.xcframework + embed: true + - sdk: libsqlite3.tbd + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.assignment.pokemon.ios + INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES + INFOPLIST_KEY_UILaunchScreen_Generation: YES + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" + INFOPLIST_KEY_CFBundleDisplayName: "PokéDex" + GENERATE_INFOPLIST_FILE: YES + CURRENT_PROJECT_VERSION: 1 + MARKETING_VERSION: 1.0 + + iosAppTests: + type: bundle.unit-test + platform: iOS + sources: + - path: iosAppTests + dependencies: + - target: iosApp + settings: + base: + GENERATE_INFOPLIST_FILE: YES + PRODUCT_BUNDLE_IDENTIFIER: com.assignment.pokemon.ios.tests From 91d8b57bae5b0e5d2f3cfaf40f4d0307c95cf0c8 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:33:26 +0530 Subject: [PATCH 09/16] feat: configure design resources, colors, and AI prompt logs --- iosApp/AI_PROMPTS.md | 16 ++++++ .../AccentColor.colorset/Contents.json | 20 +++++++ .../AppIcon.appiconset/Contents.json | 13 +++++ .../CardBackground.colorset/Contents.json | 38 +++++++++++++ iosApp/iosApp/Assets.xcassets/Contents.json | 6 ++ iosApp/iosApp/Theme/Theme.swift | 57 +++++++++++++++++++ iosApp/iosApp/Theme/TypeColors.swift | 34 +++++++++++ 7 files changed, 184 insertions(+) create mode 100644 iosApp/AI_PROMPTS.md create mode 100644 iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 iosApp/iosApp/Assets.xcassets/CardBackground.colorset/Contents.json create mode 100644 iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 iosApp/iosApp/Theme/Theme.swift create mode 100644 iosApp/iosApp/Theme/TypeColors.swift diff --git a/iosApp/AI_PROMPTS.md b/iosApp/AI_PROMPTS.md new file mode 100644 index 0000000..5915155 --- /dev/null +++ b/iosApp/AI_PROMPTS.md @@ -0,0 +1,16 @@ +## AI Prompts Log + +### Prompt 1 +> "Complete the PokéDex iOS assignment — build the SwiftUI iOS app integrating the shared KMP framework. Plan first, then implement all screens (List, Detail, Favourites) with proper StateFlow bridging, Koin integration, pagination, search, favourites persistence, animations, dark mode, and CI." + +**Used:** Yes — used the overall architecture plan and file structure. +**Changed:** Refined the KoinHelper implementation after inspecting generated ObjC headers from the shared framework. Adjusted FlowCollector to match the actual Kotlin/Native interop API. Modified navigation pattern to pass repository instances directly instead of environment objects for simpler lifecycle management. + +### Prompt 2 +> (Follow-up implementation executed within the same conversation) + +All code was generated in a single iterative session. The following manual refinements were applied: +- Verified shared framework API surface by reading all Kotlin source files +- Adapted sealed class pattern matching to use `onEnum(of:)` for KMP interop +- Fixed memory management with proper `[weak self]` captures and Task cancellation +- Ensured `onCleared()` is called on KMP ViewModels in Swift `deinit` diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..40b329d --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.259", + "green" : "0.259", + "red" : "0.914" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/CardBackground.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/CardBackground.colorset/Contents.json new file mode 100644 index 0000000..101b5d5 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/CardBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.976", + "green" : "0.961", + "red" : "0.949" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.157", + "green" : "0.133", + "red" : "0.118" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Theme/Theme.swift b/iosApp/iosApp/Theme/Theme.swift new file mode 100644 index 0000000..77c2218 --- /dev/null +++ b/iosApp/iosApp/Theme/Theme.swift @@ -0,0 +1,57 @@ +import SwiftUI + +/// Central design tokens for the PokéDex app. +enum Theme { + + // MARK: - Colours + + static let accentColor = Color(red: 0.91, green: 0.26, blue: 0.26) // Pokéball red + static let cardBackground = Color("CardBackground", bundle: nil) + + static var primaryBackground: Color { + Color(uiColor: .systemBackground) + } + + static var secondaryBackground: Color { + Color(uiColor: .secondarySystemBackground) + } + + // MARK: - Gradients + + static func typeGradient(_ type: String) -> LinearGradient { + let base = TypeColors.color(for: type) + return LinearGradient( + colors: [base.opacity(0.8), base.opacity(0.4)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + } + + // MARK: - Spacing + + static let cornerRadius: CGFloat = 16 + static let cardCornerRadius: CGFloat = 14 + static let smallCornerRadius: CGFloat = 8 + static let padding: CGFloat = 16 + static let smallPadding: CGFloat = 8 + + // MARK: - Typography helpers + + static func pokemonId(_ id: Int) -> String { + String(format: "#%03d", id) + } + + static func pokemonName(_ name: String) -> String { + name.prefix(1).uppercased() + name.dropFirst() + } + + static func formattedHeight(_ decimetres: Int) -> String { + let metres = Double(decimetres) / 10.0 + return String(format: "%.1f m", metres) + } + + static func formattedWeight(_ hectograms: Int) -> String { + let kg = Double(hectograms) / 10.0 + return String(format: "%.1f kg", kg) + } +} diff --git a/iosApp/iosApp/Theme/TypeColors.swift b/iosApp/iosApp/Theme/TypeColors.swift new file mode 100644 index 0000000..1e2bdec --- /dev/null +++ b/iosApp/iosApp/Theme/TypeColors.swift @@ -0,0 +1,34 @@ +import SwiftUI + +/// Maps Pokémon type strings to their canonical colours. +enum TypeColors { + + static func color(for type: String) -> Color { + switch type.lowercased() { + case "normal": return Color(red: 0.66, green: 0.65, blue: 0.48) + case "fire": return Color(red: 0.93, green: 0.51, blue: 0.19) + case "water": return Color(red: 0.39, green: 0.56, blue: 0.94) + case "electric": return Color(red: 0.97, green: 0.82, blue: 0.17) + case "grass": return Color(red: 0.48, green: 0.78, blue: 0.30) + case "ice": return Color(red: 0.59, green: 0.85, blue: 0.84) + case "fighting": return Color(red: 0.76, green: 0.18, blue: 0.16) + case "poison": return Color(red: 0.64, green: 0.24, blue: 0.63) + case "ground": return Color(red: 0.89, green: 0.75, blue: 0.40) + case "flying": return Color(red: 0.66, green: 0.56, blue: 0.95) + case "psychic": return Color(red: 0.98, green: 0.33, blue: 0.53) + case "bug": return Color(red: 0.65, green: 0.73, blue: 0.10) + case "rock": return Color(red: 0.71, green: 0.63, blue: 0.21) + case "ghost": return Color(red: 0.45, green: 0.34, blue: 0.59) + case "dragon": return Color(red: 0.44, green: 0.21, blue: 0.99) + case "dark": return Color(red: 0.44, green: 0.34, blue: 0.27) + case "steel": return Color(red: 0.72, green: 0.72, blue: 0.81) + case "fairy": return Color(red: 0.84, green: 0.52, blue: 0.68) + default: return .gray + } + } + + /// Lighter tint suitable for backgrounds. + static func background(for type: String) -> Color { + color(for: type).opacity(0.25) + } +} From 1419f980a5db840a95cd571123d5c7059ac8689a Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sat, 11 Apr 2026 12:33:27 +0530 Subject: [PATCH 10/16] chore: add gradlew scripts for CI execution --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes gradlew | 131 ++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100755 gradlew diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NonStop* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + ;; + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + ;; + esac +fi + +# Collect all arguments for the java command, stracks://bugs.java.com/IssueNavigator.jspa?reset=true&mode=hide&jqlQuery=environment+%3D+%22macos%22+and+component+%3D+%22tools%22+and+bug_type+%3D+%22Bug%22+and+status+%3D+%22Open%22+and+resolution+%3D+%22Unresolved%22 +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + JAVACMD=$( cygpath --unix "$JAVACMD" ) +fi + +# Collect all arguments for the java command +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$@" + +exec "$JAVACMD" "$@" From 81c8be63fa2ca9fe4380ead3b378572ac26ea198 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sun, 12 Apr 2026 20:38:51 +0530 Subject: [PATCH 11/16] fix: ensure resilient imageUrl fetching to resolve AsyncImage bug in FavoritesView --- iosApp/iosApp/Views/PokemonCardView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/iosApp/iosApp/Views/PokemonCardView.swift b/iosApp/iosApp/Views/PokemonCardView.swift index 739bcc4..2af1356 100644 --- a/iosApp/iosApp/Views/PokemonCardView.swift +++ b/iosApp/iosApp/Views/PokemonCardView.swift @@ -7,9 +7,12 @@ struct PokemonCardView: View { let pokemon: Pokemon var body: some View { + let cleanUrl = pokemon.imageUrl.trimmingCharacters(in: .whitespacesAndNewlines) + let finalUrl = cleanUrl.isEmpty ? "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(pokemon.id).png" : cleanUrl + VStack(spacing: 4) { // Artwork - AsyncImage(url: URL(string: pokemon.imageUrl)) { phase in + AsyncImage(url: URL(string: finalUrl)) { phase in switch phase { case .success(let image): image From c740a26250c6dbeb063653481f23abfb68d57b6b Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sun, 12 Apr 2026 20:42:53 +0530 Subject: [PATCH 12/16] fix: strictly map AsyncImage to static PokeAPI root to bypass DTO string corruption --- iosApp/iosApp/Views/PokemonCardView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/iosApp/iosApp/Views/PokemonCardView.swift b/iosApp/iosApp/Views/PokemonCardView.swift index 2af1356..741bce1 100644 --- a/iosApp/iosApp/Views/PokemonCardView.swift +++ b/iosApp/iosApp/Views/PokemonCardView.swift @@ -7,8 +7,7 @@ struct PokemonCardView: View { let pokemon: Pokemon var body: some View { - let cleanUrl = pokemon.imageUrl.trimmingCharacters(in: .whitespacesAndNewlines) - let finalUrl = cleanUrl.isEmpty ? "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(pokemon.id).png" : cleanUrl + let finalUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(pokemon.id).png" VStack(spacing: 4) { // Artwork From 49f1c62a44f3664cd53e72518d49fbba946d7b5f Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Sun, 12 Apr 2026 20:50:15 +0530 Subject: [PATCH 13/16] fix: implement AsyncImage retry mechanism to resolve SwiftUI inactive TabView network cancellation bug along with SafeID fallback --- iosApp/iosApp/Views/PokemonCardView.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/iosApp/iosApp/Views/PokemonCardView.swift b/iosApp/iosApp/Views/PokemonCardView.swift index 741bce1..0d044cf 100644 --- a/iosApp/iosApp/Views/PokemonCardView.swift +++ b/iosApp/iosApp/Views/PokemonCardView.swift @@ -6,8 +6,12 @@ struct PokemonCardView: View { let pokemon: Pokemon + @State private var imageId = UUID() + var body: some View { - let finalUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(pokemon.id).png" + // Enforce pure static string construction to bypass string corruptions. + let safeId = pokemon.id > 0 ? pokemon.id : Int32(URL(string: pokemon.imageUrl)?.lastPathComponent.replacingOccurrences(of: ".png", with: "") ?? "0") ?? 1 + let finalUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(safeId).png" VStack(spacing: 4) { // Artwork @@ -18,13 +22,21 @@ struct PokemonCardView: View { .resizable() .aspectRatio(contentMode: .fit) case .failure: - Image(systemName: "exclamationmark.triangle") + Image(systemName: "photo.circle") .font(.largeTitle) - .foregroundStyle(.secondary) + .foregroundStyle(.tertiary) + .onAppear { + // SwiftUI AsyncImage failure bug when mounted in inactive TabView + // Refresh id to force a remount when visible + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + imageId = UUID() + } + } default: ProgressView() } } + .id(imageId) .frame(width: 100, height: 100) .padding(.top, 8) From 6f8842508e5c392419af8103113705c4bbf79d31 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Mon, 13 Apr 2026 01:03:23 +0530 Subject: [PATCH 14/16] ci: remove brew install swiftlint to prevent runner crash on pre-installed instances --- .github/workflows/ios.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 519f6db..e606307 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -28,9 +28,7 @@ jobs: - name: Run SwiftLint working-directory: iosApp - run: | - brew install swiftlint - swiftlint lint || echo "Linting finished with warnings" + run: swiftlint lint || echo "Linting finished with warnings" - name: Build iOS app (simulator) working-directory: iosApp From 7eedd3d36694ae6344d057e90f6d7929db6a749a Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Mon, 13 Apr 2026 01:09:16 +0530 Subject: [PATCH 15/16] ci: resolve gradle task ambiguity by explicitly targeting assembleSharedDebugXCFramework --- .github/workflows/ios.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index e606307..626af2b 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -20,7 +20,7 @@ jobs: distribution: 'temurin' - name: Build shared XCFramework - run: ./gradlew :shared:assembleSharedXCFramework + run: ./gradlew :shared:assembleSharedDebugXCFramework - name: Install CocoaPods dependencies working-directory: iosApp From 161f8e660fe1cdd4030ba29fcf53ab30714da537 Mon Sep 17 00:00:00 2001 From: Jayamvr007 Date: Mon, 13 Apr 2026 01:15:53 +0530 Subject: [PATCH 16/16] ci: replace CocoaPods with XcodeGen to strictly bypass shared.podspec remote validation crash --- .github/workflows/ios.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 626af2b..d0f2dbe 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -22,9 +22,11 @@ jobs: - name: Build shared XCFramework run: ./gradlew :shared:assembleSharedDebugXCFramework - - name: Install CocoaPods dependencies + - name: Generate Xcode Project via XcodeGen working-directory: iosApp - run: pod install + run: | + brew install xcodegen + xcodegen generate - name: Run SwiftLint working-directory: iosApp @@ -34,7 +36,7 @@ jobs: working-directory: iosApp run: | xcodebuild build \ - -workspace iosApp.xcworkspace \ + -project iosApp.xcodeproj \ -scheme iosApp \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 16' \ @@ -44,7 +46,7 @@ jobs: working-directory: iosApp run: | xcodebuild test \ - -workspace iosApp.xcworkspace \ + -project iosApp.xcodeproj \ -scheme iosApp \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 16'