Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Combine on a SwiftUI View

Tags:

ios

swift

swiftui

This question relates to this one: How to observe a TextField value with SwiftUI and Combine?

But what I am asking is a bit more general. Here is my code:

struct MyPropertyStruct {
    var text: String
}

class TestModel : ObservableObject {
    @Published var myproperty = MyPropertyStruct(text: "initialText")

    func saveTextToFile(text: String) {
        print("this function saves text to file")
    }
}

struct ContentView: View {
    @ObservedObject var testModel = TestModel()
    var body: some View {
        TextField("", text: $testModel.myproperty.text)
    }
}

Scenario: As the user types into the textfield, the saveTextToFile function should be called. Since this is saving to a file, it should be slowed-down/throttled.

So My question is:

  1. Where is the proper place to put the combine operations in the code below.
  2. What Combine code do I put to accomplish: (A) The string must not contain spaces. (B) The string must be 5 characters long. (C) The String must be debounced/slown down

I wanted to use the response here to be a general pattern of: How should we handle combine stuff in a SwiftUI app (not UIKit app).

like image 962
Just a coder Avatar asked Sep 13 '19 11:09

Just a coder


People also ask

What is combine framework in SwiftUI?

Overview. The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.

Is combine only for SwiftUI?

Certainly from the requirements of the system, any system that can run Combine can run SwiftUI, but I know engineers that are comfortable with UIKit, and they don't want to go to SwiftUI yet. They've been using React or something, so they do want to move over to Combine.

Is SwiftUI better than storyboard?

Conclusion. After looking at the pros and cons of both Storyboard and SwiftUI, according to my opinion building app UI using storyboard is comparatively easier for a beginner, and has been there was a very long time, but there are some flaws with it and that flaws have been taken care of in Swift UI.

Can you mix SwiftUI and UIKit?

The hosting controller is compatible with UIKit since it is a subclass of UIViewController . The purpose of the UIHostingController is to enclose a SwiftUI view so that it can be integrated into an existing UIKit based project.

What is the combining effect of SwiftUI and combine?

Use the combining effect of SwiftUI and Combine to produce a well-structured, efficient, and maintainable code. This SwiftUI + Combine tutorial was originally published in December 2019 and was updated in December 2020 to make it more relevant and comprehensive.

How does onreceive work in SwiftUI?

SwiftUI views provide the onReceive modifier, which has two arguments: the Publisher from Combine framework and the closure. SwiftUI subscribes to the publisher and runs passed closure whenever the publisher emits the value. Let’s take a look at the sample code now.

How does SwiftUI work in Swift?

SwiftUI subscribes to the publisher and runs passed closure whenever the publisher emits the value. Let’s take a look at the sample code now.

Should we use property wrappers or combine publishers in SwiftUI?

While using those property wrappers is certainly the preferred approach in the vast majority of cases, another option that can be good to keep in mind is that we can also observe Combine publishers directly within our SwiftUI views as well.


1 Answers

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)")
    }
}
like image 109
matteopuc Avatar answered Oct 25 '22 08:10

matteopuc