You should do what you want in your ViewModel
. Your view model is the TestModel
class (which I suggest you rename it in TestViewModel
). It's where you are supposed to put the logic between the model and the view. The ViewModel
should prepare the model to be ready for the visualization. And that is the right place to put your combine logic (if it's related to the view, of course).
Now we can use your specific example to actually make an example. To be honest there are a couple of slight different solutions depending on what you really want to achieve. But for now I'll try to be as generic as possible and then you can tell me if the solution is fine or it needs some refinements:
struct MyPropertyStruct {
var text: String
}
class TestViewModel : ObservableObject {
@Published var myproperty = MyPropertyStruct(text: "initialText")
private var canc: AnyCancellable!
init() {
canc = $myproperty.debounce(for: 0.5, scheduler: DispatchQueue.main).sink { [unowned self] newText in
let strToSave = self.cleanText(text: newText.text)
if strToSave != newText.text {
//a cleaning has actually happened, so we must change our text to reflect the cleaning
self.myproperty.text = strToSave
}
self.saveTextToFile(text: strToSave)
}
}
deinit {
canc.cancel()
}
private func cleanText(text: String) -> String {
//remove all the spaces
let resultStr = String(text.unicodeScalars.filter {
$0 != " "
})
//take up to 5 characters
return String(resultStr.prefix(5))
}
private func saveTextToFile(text: String) {
print("text saved")
}
}
struct ContentView: View {
@ObservedObject var testModel = TestViewModel()
var body: some View {
TextField("", text: $testModel.myproperty.text)
}
}
You should attach your own subscriber
to the TextField
publisher
and use the debounce
publisher to delay the cleaning of the string and the calling to the saving method. According to the documentation:
debounce(for:scheduler:options:)
Use this operator when you want to wait for a pause in the delivery of
events from the upstream publisher. For example, call debounce on the
publisher from a text field to only receive elements when the user
pauses or stops typing. When they start typing again, the debounce
holds event delivery until the next pause.
When the user stops typing the debounce publisher waits for the specified time (in my example here above 0.5 secs) and then it calls its subscriber with the new value.
The solution above delays both the saving of the string and the TextField
update. This means that users will see the original string (the one with spaces and maybe longer than 5 characters) for a while, before the update happens. And that's why, at the beginning of this answer, I said that there were a couple of different solutions depending on the needs. If, indeed, we want to delay just the saving of the string, but we want the users to be forbidden to input space characters or string longer that 5 characters, we can use two subscribers (I'll post just the code that changes, i.e. the TestViewModel
class):
class TestViewModel : ObservableObject {
@Published var myproperty = MyPropertyStruct(text: "initialText")
private var saveCanc: AnyCancellable!
private var updateCanc: AnyCancellable!
init() {
saveCanc = $myproperty.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { [unowned self] in self.cleanText(text: $0.text) }
.sink { [unowned self] newText in
self.saveTextToFile(text: self.cleanText(text: newText))
}
updateCanc = $myproperty.sink { [unowned self] newText in
let strToSave = self.cleanText(text: newText.text)
if strToSave != newText.text {
//a cleaning has actually happened, so we must change our text to reflect the cleaning
DispatchQueue.main.async {
self.myproperty.text = strToSave
}
}
}
}
deinit {
saveCanc.cancel()
updateCanc.cancel()
}
private func cleanText(text: String) -> String {
//remove all the spaces
let resultStr = String(text.unicodeScalars.filter {
$0 != " "
})
//take up to 5 characters
return String(resultStr.prefix(5))
}
private func saveTextToFile(text: String) {
print("text saved: \(text)")
}
}