简体   繁体   English

如何在SwiftUI视图上使用Combine

[英]How to use Combine on a SwiftUI View

This question relates to this one: How to observe a TextField value with SwiftUI and Combine? 这个问题与此有关: 如何使用SwiftUI和Combine观察TextField的值?

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. 场景:随着用户在文本字段中键入内容,应该调用saveTextToFile函数。 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. 我要完成什么组合代码: (A)字符串不能包含空格。 (B) The string must be 5 characters long. (B)字符串必须为5个字符长。 (C) The String must be debounced/slown down (C)弦必须去抖动/掉线

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). 我想在这里使用响应作为以下常规模式: 我们应该如何处理SwiftUI应用程序(而非UIKit应用程序)中的组合内容。

You should do what you want in your ViewModel . 您应该在ViewModel Your view model is the TestModel class (which I suggest you rename it in TestViewModel ). 您的视图模型是TestModel类(建议您在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. ViewModel应该准备好模型以便进行可视化。 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. 您应该将您自己的subscriber附加到TextField publisher并使用debounce发布者来延迟字符串的清理和对save方法的调用。 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. 当用户停止键入时,防反弹发布者将等待指定的时间(在我的示例中为0.5秒以上),然后使用新值调用其订阅者。

The solution above delays both the saving of the string and the TextField update. 以上的延迟解决方案的字符串节约 TextField更新。 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. 这意味着在更新发生之前,用户会看到原始字符串(带有空格且可能超过5个字符的字符串)一段时间。 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, ie the TestViewModel class): 如果确实确实要延迟字符串的保存,但是我们希望禁止用户输入空格字符或长度超过5个字符的字符串,则可以使用两个订阅者(我将仅发布更改的代码,即TestViewModel类):

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)")
    }
}
import Combine

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)
            .map { $0.text.replacingOccurrences(of: " ", with: "")}
            .map { String($0.prefix(5)) }
            .sink { [unowned self] strToSave in
                self.saveTextToFile(text: strToSave)
        }
    }

    private func saveTextToFile(text: String) {
        print("text saved")
    }
}

struct ContentView: View {
    @ObservedObject var testModel = TestViewModel()

    var body: some View {
        TextField("", text: $testModel.myproperty.text)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM