diff --git a/ios/KaMPKitiOS.xcodeproj/project.pbxproj b/ios/KaMPKitiOS.xcodeproj/project.pbxproj index 913b1b11..9d39915c 100644 --- a/ios/KaMPKitiOS.xcodeproj/project.pbxproj +++ b/ios/KaMPKitiOS.xcodeproj/project.pbxproj @@ -7,10 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C73D0492A335103003E6929 /* BreedsViewModel.swift */; }; 3CD290EC2A251417004C7AD1 /* KMPNativeCoroutinesCombine in Frameworks */ = {isa = PBXBuildFile; productRef = 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */; }; 3CD290EE2A251417004C7AD1 /* KMPNativeCoroutinesCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3CD290ED2A251417004C7AD1 /* KMPNativeCoroutinesCore */; }; 3DFF917C64A18A83DA010EE1 /* Pods_KaMPKitiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B859F3FB23133D22AB9DD835 /* Pods_KaMPKitiOS.framework */; }; - 46A5B5EF26AF54F7002EFEAA /* BreedListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */; }; + 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */; }; 46A5B60826B04921002EFEAA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46A5B60626B04920002EFEAA /* Main.storyboard */; }; 46B5284D249C5CF400A7725D /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B5284C249C5CF400A7725D /* Koin.swift */; }; F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */; }; @@ -40,7 +41,8 @@ /* Begin PBXFileReference section */ 1DFCC00C8DAA719770A18D1A /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; }; 2A1ED6A4A2A53F5F75C58E5F /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; }; - 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedListScreen.swift; sourceTree = ""; }; + 3C73D0492A335103003E6929 /* BreedsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsViewModel.swift; sourceTree = ""; }; + 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsScreen.swift; sourceTree = ""; }; 46A5B60726B04920002EFEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46B5284C249C5CF400A7725D /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; B859F3FB23133D22AB9DD835 /* Pods_KaMPKitiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KaMPKitiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -87,6 +89,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C73D0482A335061003E6929 /* Breeds */ = { + isa = PBXGroup; + children = ( + 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */, + 3C73D0492A335103003E6929 /* BreedsViewModel.swift */, + ); + path = Breeds; + sourceTree = ""; + }; 6278498AD96A4D949D39BF44 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -131,13 +142,13 @@ F1465EFF23AA94BF0055F7C3 /* KaMPKitiOS */ = { isa = PBXGroup; children = ( + 3C73D0482A335061003E6929 /* Breeds */, F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */, 46A5B60626B04920002EFEAA /* Main.storyboard */, 46B5284C249C5CF400A7725D /* Koin.swift */, F1465F0923AA94BF0055F7C3 /* Assets.xcassets */, F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */, F1465F0E23AA94BF0055F7C3 /* Info.plist */, - 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */, ); path = KaMPKitiOS; sourceTree = ""; @@ -362,7 +373,8 @@ buildActionMask = 2147483647; files = ( 46B5284D249C5CF400A7725D /* Koin.swift in Sources */, - 46A5B5EF26AF54F7002EFEAA /* BreedListScreen.swift in Sources */, + 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */, + 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */, F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/KaMPKitiOS/AppDelegate.swift b/ios/KaMPKitiOS/AppDelegate.swift index 25924195..139d7889 100644 --- a/ios/KaMPKitiOS/AppDelegate.swift +++ b/ios/KaMPKitiOS/AppDelegate.swift @@ -22,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { startKoin() - let viewController = UIHostingController(rootView: BreedListScreen()) + let viewController = UIHostingController(rootView: BreedsScreen()) self.window = UIWindow(frame: UIScreen.main.bounds) self.window?.rootViewController = viewController diff --git a/ios/KaMPKitiOS/BreedListScreen.swift b/ios/KaMPKitiOS/BreedListScreen.swift deleted file mode 100644 index e02201ee..00000000 --- a/ios/KaMPKitiOS/BreedListScreen.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// BreedListView.swift -// KaMPKitiOS -// -// Created by Russell Wolf on 7/26/21. -// Copyright © 2021 Touchlab. All rights reserved. -// - -import Combine -import SwiftUI -import shared -import KMPNativeCoroutinesCombine - -private let log = koin.loggerWithTag(tag: "ViewController") - -class ObservableBreedModel: ObservableObject { - private var viewModel: BreedViewModel? - - @Published - var loading = false - - @Published - var breeds: [Breed]? - - @Published - var error: String? - - private var cancellables = [AnyCancellable]() - - func activate() { - let viewModel = KotlinDependencies.shared.getBreedViewModel() - - createPublisher(for: viewModel.breedStateFlow) - .sink { _ in } receiveValue: { [weak self] (breedState: BreedViewState) in - self?.loading = breedState.isLoading - self?.breeds = breedState.breeds - self?.error = breedState.error - - if let breeds = breedState.breeds { - log.d(message: {"View updating with \(breeds.count) breeds"}) - } - if let errorMessage = breedState.error { - log.e(message: {"Displaying error: \(errorMessage)"}) - } - } - .store(in: &cancellables) - - self.viewModel = viewModel - } - - func deactivate() { - viewModel?.clear() - } - - func onBreedFavorite(_ breed: Breed) { - viewModel?.updateBreedFavorite(breed: breed) - } - - func refresh() { - viewModel?.refreshBreeds() - } -} - -struct BreedListScreen: View { - @StateObject - var observableModel = ObservableBreedModel() - - var body: some View { - BreedListContent( - loading: observableModel.loading, - breeds: observableModel.breeds, - error: observableModel.error, - onBreedFavorite: { observableModel.onBreedFavorite($0) }, - refresh: { observableModel.refresh() } - ) - .onAppear(perform: { - observableModel.activate() - }) - .onDisappear(perform: { - observableModel.deactivate() - }) - } -} - -struct BreedListContent: View { - var loading: Bool - var breeds: [Breed]? - var error: String? - var onBreedFavorite: (Breed) -> Void - var refresh: () -> Void - - var body: some View { - ZStack { - VStack { - if let breeds = breeds { - List(breeds, id: \.id) { breed in - BreedRowView(breed: breed) { - onBreedFavorite(breed) - } - } - } - if let error = error { - Text(error) - .foregroundColor(.red) - } - Button("Refresh") { - refresh() - } - } - if loading { Text("Loading...") } - } - } -} - -struct BreedRowView: View { - var breed: Breed - var onTap: () -> Void - - var body: some View { - Button(action: onTap) { - HStack { - Text(breed.name) - .padding(4.0) - Spacer() - Image(systemName: (!breed.favorite) ? "heart" : "heart.fill") - .padding(4.0) - } - } - } -} - -struct BreedListScreen_Previews: PreviewProvider { - static var previews: some View { - BreedListContent( - loading: false, - breeds: [ - Breed(id: 0, name: "appenzeller", favorite: false), - Breed(id: 1, name: "australian", favorite: true) - ], - error: nil, - onBreedFavorite: { _ in }, - refresh: {} - ) - } -} diff --git a/ios/KaMPKitiOS/Breeds/BreedsScreen.swift b/ios/KaMPKitiOS/Breeds/BreedsScreen.swift new file mode 100644 index 00000000..1b0c8e33 --- /dev/null +++ b/ios/KaMPKitiOS/Breeds/BreedsScreen.swift @@ -0,0 +1,90 @@ +// +// BreedListView.swift +// KaMPKitiOS +// +// Created by Russell Wolf on 7/26/21. +// Copyright © 2021 Touchlab. All rights reserved. +// + +import SwiftUI +import shared + +struct BreedsScreen: View { + @StateObject + var viewModel = BreedsViewModel() + + var body: some View { + BreedsContent( + state: viewModel.state, + onBreedFavorite: { viewModel.onBreedFavorite($0) }, + refresh: { viewModel.refresh() } + ) + .onAppear(perform: { + viewModel.subscribeState() + }) + .onDisappear(perform: { + viewModel.unsubscribeState() + }) + } +} + +struct BreedsContent: View { + var state: BreedViewState + var onBreedFavorite: (Breed) -> Void + var refresh: () -> Void + + var body: some View { + ZStack { + VStack { + List(state.breeds, id: \.id) { breed in + BreedRowView(breed: breed) { + onBreedFavorite(breed) + } + } + if let error = state.error { + Text(error) + .foregroundColor(.red) + } + Button("Refresh") { + refresh() + } + } + if state.isLoading { Text("Loading...") } + } + } +} + +struct BreedRowView: View { + var breed: Breed + var onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack { + Text(breed.name) + .padding(4.0) + Spacer() + Image(systemName: (!breed.favorite) ? "heart" : "heart.fill") + .padding(4.0) + } + } + } +} + +struct BreedsScreen_Previews: PreviewProvider { + static var previews: some View { + BreedsContent( + state: BreedViewState( + breeds: [ + Breed(id: 0, name: "appenzeller", favorite: false), + Breed(id: 1, name: "australian", favorite: true) + ], + error: nil, + isLoading: false, + isEmpty: false + ), + onBreedFavorite: { _ in }, + refresh: {} + ) + } +} diff --git a/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift new file mode 100644 index 00000000..c4ea2b2e --- /dev/null +++ b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift @@ -0,0 +1,45 @@ +// +// BreedsViewModel.swift +// KaMPKitiOS +// +// Created by Bartłomiej Pedryc on 09/06/2023. +// Copyright © 2023 Touchlab. All rights reserved. +// + +import Combine +import Foundation +import shared +import KMPNativeCoroutinesCombine + +class BreedsViewModel: ObservableObject { + + @Published var state: BreedViewState = BreedViewState.companion.default() + + private var viewModelDelegate: BreedViewModelDelegate = KotlinDependencies.shared.getBreedViewModel() + private var cancellables = [AnyCancellable]() + + deinit { + viewModelDelegate.clear() + } + + func subscribeState() { + createPublisher(for: viewModelDelegate.breedStateFlow) + .sink { _ in } receiveValue: { [weak self] (breedState: BreedViewState) in + self?.state = breedState + } + .store(in: &cancellables) + } + + func unsubscribeState() { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + + func onBreedFavorite(_ breed: Breed) { + viewModelDelegate.updateBreedFavorite(breed: breed) + } + + func refresh() { + viewModelDelegate.refreshBreeds() + } +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt index 8f8ea556..27567f07 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt @@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlin.native.ObjCName +@ObjCName("BreedViewModelDelegate") class BreedViewModel( private val breedRepository: BreedRepository, log: Logger ) : ViewModel() { - private val log = log.withTag("BreedCommonViewModel") + private val log = log.withTag("BreedViewModel") private val mutableBreedState: MutableStateFlow = MutableStateFlow(BreedViewState(isLoading = true)) @@ -53,7 +55,7 @@ class BreedViewModel( } BreedViewState( isLoading = false, - breeds = breeds.takeIf { it.isNotEmpty() }, + breeds = breeds, error = errorMessage.takeIf { breeds.isEmpty() }, isEmpty = breeds.isEmpty() && errorMessage == null ) @@ -84,7 +86,7 @@ class BreedViewModel( private fun handleBreedError(throwable: Throwable) { log.e(throwable) { "Error downloading breed list" } mutableBreedState.update { - if (it.breeds.isNullOrEmpty()) { + if (it.breeds.isEmpty()) { BreedViewState(error = "Unable to refresh breed list") } else { // Just let it fail silently if we have a cache @@ -95,8 +97,14 @@ class BreedViewModel( } data class BreedViewState( - val breeds: List? = null, + val breeds: List = emptyList(), val error: String? = null, val isLoading: Boolean = false, val isEmpty: Boolean = false -) +) { + companion object { + // This method lets you use the default constructor values in Swift. When accessing the + // constructor directly, they will not work there and would need to be provided explicitly. + fun default() = BreedViewState() + } +}