rickclephas / KMP-ObservableViewModel

Library to use AndroidX/Kotlin ViewModels with SwiftUI
MIT License
570 stars 28 forks source link

Boolean toggle doesn't re-render swift view #4

Closed JonasHiltl closed 1 year ago

JonasHiltl commented 1 year ago

I'm playing around a bit with KMM-Viewmodel and SwiftUI and for example sharing state for a TextField works great but updating a boolean does not re-render the view in SwiftUI.

This is my example ViewModel that holds the showPassword boolean and some extra state.

open class LoginViewModel : KMMViewModel() {
    var email: String = ""
    var password: String = ""
    var showPassword: Boolean = false

    fun changeEmail(email: String) {
        this.email = email
    }

    fun changePassword(password: String) {
        this.password = password
    }

    fun toggleShowPassword() {
        showPassword = !showPassword
    }
}

This is how I toggle the showPassword boolean in my iOS app and I expected the view to re-render and the two TextFields and the icon to switch but that doesn't happen.

...
VStack(spacing: Padding.l) {
    TextField(
        "Email",
         text: $loginViewModel.email
    )

    Group {
        if loginViewModel.showPassword {
            TextField(
              "Password",
              text: Binding(get: {loginViewModel.password}, set: loginViewModel.changePassword)
            )
        } else {
            SecureField(
                "Password",
                text: Binding(get: {loginViewModel.password}, set: loginViewModel.changePassword)
            )
        }
    }

    Button(
        action: { loginViewModel.toggleShowPassword() },
        label: {
            Image(systemName: loginViewModel.showPassword ? "eye" : "eye.slash")
        }
    )
}

Is this behaviour expected and just not yet supported or am I doing something wrong?

rickclephas commented 1 year ago

Hi. Yeah with the current ViewModel that is to be expected. In order for state changes to propagate to SwiftUI you'll need to make use of StateFlows. Specifically the StateFlow functions from KMM-ViewModel that accept the ViewModelScope.

import com.rickclephas.kmm.viewmodel.KMMViewModel
import com.rickclephas.kmm.viewmodel.MutableStateFlow

open class LoginViewModel : KMMViewModel() {
    var email = MutableStateFlow(viewModelScope, "")
    var password = MutableStateFlow(viewModelScope, "")
    var showPassword = MutableStateFlow(viewModelScope, false)
}

To access the StateFlow value from SwiftUI you can either create extension properties like the following (from the sample): https://github.com/rickclephas/KMM-ViewModel/blob/c9593954b5c62b4ccc35df2c9113bb04f98f0ea9/sample/shared/src/iosMain/kotlin/com/rickclephas/kmm/viewmodel/sample/shared/TimeTravelViewModel.kt#L3-L4

Alternatively you could use KMP-NativeCoroutines, which will generate these for you.

P.S. if you are already using the Kotlin 1.8.0 release candidate you'll be able to use the @NativeCoroutinesState annotation (from KMP-NativeCoroutines) which turns your StateFlow properties into "regular" properties in Swift. An example can be seen in the Kotlin 1.8.0 PR.

JonasHiltl commented 1 year ago

Thanks for clearing that up so quickly and I will upgrade to 1.8.0 RC and try it out.