Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Call objectWillChange.send() into Binding setter when use viewModel.binging #221

Open
PGochachko opened this issue Jan 4, 2023 · 1 comment

Comments

@PGochachko
Copy link
Contributor

PGochachko commented Jan 4, 2023

Hello!

I noticed strange behavior when using the "binding" function of the ViewModel in swift (iOS). This happens when masking a value in a TextField.

Code sample:

SwiftUIView.swift

import SwiftUI
import MultiPlatformLibrary

struct SwiftUIView: View {
    @StateObject
    private var viewModel: ExampleViewModel = ExampleViewModel()
    
    var body: some View {
        TextField("Input text",
                  text: viewModel.binding(\.text,
                                           equals: { $0 == $1 },
                                           getMapper: { viewModel.mask(value: $0 as String) },
                                           setMapper: { viewModel.unmask(value: $0) as NSString }
                                         )
        )
        .padding()
    }
}

ExampleViewModel.kt

import dev.icerock.moko.mvvm.flow.CMutableStateFlow
import dev.icerock.moko.mvvm.flow.cMutableStateFlow
import dev.icerock.moko.mvvm.viewmodel.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow

class ExampleViewModel: ViewModel() {
    companion object {
        const val MASK_CHAR = '#'
    }

    val text: CMutableStateFlow<String> = MutableStateFlow("")
        .cMutableStateFlow()

    fun mask(value: String): String {
        val mask = "##.##.##"
        val stringBuilder = StringBuilder(mask.length)
        var vIndex = 0

        for (mc in mask) {
            val c = value.getOrNull(vIndex) ?: break
            when (mc) {
                MASK_CHAR, c -> {
                    stringBuilder.append(c)
                    vIndex++
                }
                else -> stringBuilder.append(mc)
            }
        }

        return stringBuilder.toString()
    }

    fun unmask(value: String): String {
        val mask = "##.##.##"
        val stringBuilder = StringBuilder(mask.count { it == MASK_CHAR })

        for (i in value.take(mask.length).indices) {
            val c = value[i]
            val mc = mask.getOrNull(i)

            if (c != mc) {
                stringBuilder.append(c)
            }
        }

        return stringBuilder.toString()
    }
}

When I enter "112233" I get the result "11.22.33". But as soon as I double click on "3" again, I get the result "11.22.333" in ui, but the variable has the correct value "11.22.33".

This problem is related to the fact that stateFlow.value does not change if this value is already set to value, and self.objectWillChange.send() is called only when stateFlow.value is updated inside the "binding" function.

The simplest fix that came to mind based on your source code is this:
Append self.objectWillChange.send() on set into Binding

func binding<T, R>(
    _ flowKey: KeyPath<Self, CMutableStateFlow<T>>,
    equals: @escaping (T?, T?) -> Bool,
    getMapper: @escaping (T) -> R,
    setMapper: @escaping (R) -> T
) -> Binding<R> {
    let stateFlow: CMutableStateFlow<T> = self[keyPath: flowKey]
    var lastValue: T? = stateFlow.value
    
    var disposable: DisposableHandle? = nil
    
    disposable = stateFlow.subscribe(onCollect: { value in
        if !equals(lastValue, value) {
            lastValue = value
            self.objectWillChange.send()
            disposable?.dispose()
        }
    })
    
    return Binding(
        get: { getMapper(stateFlow.value!) },
        set: {
            stateFlow.value = setMapper($0)
            self.objectWillChange.send()
        }
    )
}

If you see fit, please add to the library.

Thanks!

@PGochachko
Copy link
Contributor Author

I'm testing this fix on another example - it's bad fix :)
It may be necessary to abandon this situation, or think about another solution...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant