如何在双向绑定 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) -> String
这是一个同步的纯函数(无副作用),只要接收到输入操作就会调用它。因为它是一个纯函数,所以测试非常简单,不需要模拟。
最后,视图模型更新其状态并生成视图状态,视图会相应地对其做出反应(绘制单一事实来源!)。 由于 viewState 是模型状态的函数,
static func view(state: MyViewModel.State) -> ViewState
这也是一个纯函数,视图状态也可以很容易地测试。
请注意,通过视图状态绑定没有“双向绑定”。相反,视图状态被它们相应渲染的视图捕获为常量。视图不执行逻辑,它们向视图模型发送操作。
视图仅(很少)在必须管理私有内部状态时使用@State
变量。
【讨论】:
以上是关于如何在双向绑定 ViewModel/TextField 中保持 @Published 属性标准化(即保持小写,删除链接等)?的主要内容,如果未能解决你的问题,请参考以下文章