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()
}
)
}
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
ExampleViewModel.kt
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 intoBinding
If you see fit, please add to the library.
Thanks!