rickclephas/KMP-ObservableViewModel

Mapping of shared ViewModel UI State in Swift not working

jflavio11 opened this issue · 2 comments

Hello!
I need to map a list of items that come from the shared View Model into objects that implement the Identifiable protocol (needed for showing them in a List or pass them to Maps and display markers).
This is the exposed state from the ViewModel in Kotlin:

// ui state data class
data class HomeUiState(
    val isLoading: Boolean = true,
    val closeVenues: List<Venue> = emptyList(),
    val errorMessage: String? = null
)

// state defined in ViewModel.kt inside shared module
    @NativeCoroutinesState
    val uiState = _uiState.asStateFlow().stateIn(
        viewModelScope, SharingStarted.WhileSubscribed(), HomeUiState()
    )

And, in my iOS app, I'm trying to map it like this:

class HomeViewModel : shared.HomeViewModel {
    
    @Published var venues: [UiVenue] = []
    
    override init() {
        super.init()
        venues = uiState.closeVenues.compactMap { venue in
            venue.toUiVenue() // the UiVenue struct implements Identifiable protocol
        }
    }
    
}

...

// ContentView.swift

@ObservedViewModel var viewModel = HomeViewModel()

var body: some View {

            List(viewModel.venues) { venue in
                Text("Hello \(venue.name)").foregroundColor(Color.black)
            }.onAppear {
                 viewModel.getCloseVenues()
            }

}

However, the list is not rendering. I've tried using @State also, but nothing happens. The uiState.closeVenues returns a simple [Venue]object, not a Combine Observable.

However, if I use directly viewModel.uiState.closeVenues from the view, it works fine 🤔

Environment

  • Kotlin: 1.8.0
  • commonMain KMMViewModel dependency: com.rickclephas.kmm:kmm-viewmodel-core:1.0.0-ALPHA-3
  • nativeCoroutines: 1.0.0-ALPHA-4
  • ksp: 1.8.0-1.0.9

PS: this library is great, it has saved me a lot of time

Hi, glad to hear you find the library useful!

@NativeCoroutinesState converts your properties to two separate properties. In your case uiState and uiStateFlow.
The first exposes the current value of the StateFlow and the second exposes a callback wrapper for the Flow.

You can use the KMP-NativeCoroutines Combine wrappers to create a Combine publisher for the uiStateFlow.

Something like the following should work:

import KMPNativeCoroutinesCombine

class HomeViewModel : shared.HomeViewModel {
    
    @Published var venues: [UiVenue] = []
    
    override init() {
        super.init()
        createPublisher(for: uiStateFlow).map { 
            $0.closeVenues.compactMap { venue in
                venue.toUiVenue()
            }
        }.assertNoFailure().assign(to: &$venues)
    }
}

Thank you, @rickclephas

The following was working:

// ContentView.swift

            @SwiftUI.State private var venues: [UiVenue] = []

            ...
            
            View()
            .onChange(of: viewModel.uiState) { updated in
                venues = updated.closeVenues.map({ venue in
                    venue.toUiVenue()
                })
            }

However, your approach is way clean. Thank you so much!