如何在双向绑定 ViewModel/TextField 中保持 @Published 属性标准化(即保持小写,删除链接等)?

Posted

技术标签:

【中文标题】如何在双向绑定 ViewModel/TextField 中保持 @Published 属性标准化(即保持小写,删除链接等)?【英文标题】:How to keep a @Published property normalized in a two-way binding ViewModel/TextField (i.e keeping in lowercase, remove links, etc)? 【发布时间】:2021-07-08 20:44:19 【问题描述】:

保持@Published 属性标准化的最佳实践或技术是什么?假设我们有一个 ViewModel 公开了一个 @Published 文本属性,该属性可以从 ViewModel / TextField (SwiftUI) 更新,并且我们想要删除用户在该字段中输入的任何链接。

我用测试编写了这个示例,但我的问题之一是在订阅测试时,我以错误的顺序接收值(听起来改变 handleEvents 中的文本不太正确)

class ViewModel: ObservableObject 
    @Published var text: String?
    private var cancellables = Set<AnyCancellable>()

    init() 
        setupObservers()
    

    private func setupObservers() 
        $text
            .removeDuplicates() // use removeDuplicates to avoid infinite loop
            .compactMap( $0 )
            .handleEvents(receiveOutput:  [weak self] in self?.cleanupLinks(from: $0) )
            .sink(receiveValue:  _ in )
            .store(in: &cancellables)
    

    private func cleanupLinks(from text: String) 
        //dummy implementation
        self.text = text.replacingOccurrences(of: "https://any-url.com", with: "")
    


// TEST
class SimpleReproTests: XCTestCase 
    func test_text_cleanupLinks() 
        let sut = ViewModel()

        let exp = expectation(description: "Wait for text updates")
        exp.expectedFulfillmentCount = 2
        var receivedValues = [String?]()
        let cancellable = sut.$text.dropFirst(2).sink 
            receivedValues.append($0)
            exp.fulfill()
        
    
        sut.text = "Hello World! https://any-url.com"
        wait(for: [exp], timeout: 1.0)

        XCTAssertEqual(receivedValues, ["Hello World! https://any-url.com", "Hello World! "])
        cancellable.cancel()
   

现在,在运行测试时,我收到乱序的值(哪个 ofc 不符合预期):

["Hello World! ", "Hello World! https://any-url.com"]

【问题讨论】:

【参考方案1】:

没有最好的方法;)

当视图变得比最简单的用例更复杂时,我会这样做:

通常,您将所有逻辑放入视图模型中。视图模型可以通过本主题之外的各种方式实现。

基本上,视图模型接收一个“事件”可选地携带附加信息,例如每当用户键入并打算修改当前字符串时的“待修改”字符串。然后视图模型开始根据这个事件和视图模型管理的当前“状态”执行它的逻辑。这会导致视图观察并相应呈现的新“视图状态”(在您的示例中为字符串)。这样,视图模型就可以完全控制用户看到的内容,这实际上是“单一事实来源”当前值的表示。

在 SwiftUI 中,您可以使用一些父视图来完成此操作,该视图将使用视图模型进行初始化并观察它。在其主体中,它将视图状态(或相关的子状态)传递给其子视图。这些子视图接收将其保存为常量值的状态。此外,父视图传递“动作回调”,子视图调用用户操作,例如键入字符或点击按钮:

struct MyView: View 
    let state: String
    let typing: (String) -> Void
    let dismiss: () -> Void

    var body: some View  ... 

父视图将子视图的回调与视图模型的操作联系起来。它也永远不会修改 viewModel 的 viewState。这是一个例子:

struct ParentView: View 
    @StateObject var viewModel = MyViewModel()

    var body: some View 
        MyView(state: viewModel.viewState.input,
               typing:  viewModel.send(.typing($0)) , 
               dismiss:  viewModel.send(.dismiss) )
    

也就是说,为了将视图与视图模型连接起来,父视图只需调用视图模型上的一个函数,即:

func send(_ action: Action)

Action 特意是一个枚举:

enum Action 
    case typing(String) 
    case dismiss

因此,您的视图模型现在可能具有执行此“规范化”的某些功能:

static func normalise(_ input: String) -&gt; String

这是一个同步的纯函数(无副作用),只要接收到输入操作就会调用它。因为它是一个纯函数,所以测试非常简单,不需要模拟。

最后,视图模型更新其状态并生成视图状态,视图会相应地对其做出反应(绘制单一事实来源!)。 由于 viewState 是模型状态的函数,

static func view(state: MyViewModel.State) -&gt; ViewState

这也是一个纯函数,视图状态也可以很容易地测试。

请注意,通过视图状态绑定没有“双向绑定”。相反,视图状态被它们相应渲染的视图捕获为常量。视图不执行逻辑,它们向视图模型发送操作。

视图仅(很少)在必须管理私有内部状态时使用@State 变量。

【讨论】:

以上是关于如何在双向绑定 ViewModel/TextField 中保持 @Published 属性标准化(即保持小写,删除链接等)?的主要内容,如果未能解决你的问题,请参考以下文章

实现双向数据绑定

Vue3的双向绑定是如何实现的

vue数据双向绑定原理

如何在原生微信小程序中实现数据双向绑定

vue数据双向绑定原理

如何在Vue2中实现组件props双向绑定