首页 > 解决方案 > 如何使用 UITextFields 和 RxSwift 实现简单的表单?

问题描述

我是 RxSwift 的新手,我想我做的非常非常错误,所以请赐教!

简化示例:

有一个包含三个 UITextField 和一个 UIButton 的视图,以及两个用于 GET 和 PUT UserData 的 Swagger-API 调用(具有三个字符串的结构)。

var firstName = UITextField()
var lastName = UITextField()
var email = UITextField()
var sendButton = UIButton()

ViewModel 有一个 BehaviorSubject 来保存数据:

var userData = BehaviorSubject<UserData?>(value: nil)

将传入数据绑定到 BehaviorSubject 有效:

self.userData.onNext(response.userData)

并填充 UITextFields 作品:

viewModel.userData.bind { userData in
    firstName.text = userData?.firstName
    lastName.text = userData?.lastName
    email.text = userData?.email
}.disposed(by: disposeBag)

但是在 UITextFields 中输入失败,代码如下:

// changing any of these fields should trigger an update of the whole object
BehaviorSubject.combineLatest(
    self.firstName.rx.text,
    self.lastName.rx.text,
    self.email.rx.text
)
.map {
    return UserData(
        firstName: $0,
        lastName: $1,
        email: $2
    )
}
.bind(to: viewModel.userData)
.disposed(by: disposeBag)

大多数情况下,键入的框会保留,但所有其他框都被清空,或者有时都被清空,或者发生其他奇怪的事情,比如值在字段周围跳跃。

对于发送数据我使用了一个简单的点击处理程序,感觉不是很Rx:

saveButton.onTapHandler = { [weak self] in
    containerView.endEditing(true)
        do {
            try self?.viewModel.saveUserData()
        } catch {
            self?.showAlert(error)
        }
    }
}

欢迎任何帮助,也不确定 BehaviorSubject 是否正确,如果 combineLatest 满足我的需要,...

附加信息:我知道我可以为每个主题创建一个单独的 BehaviorSubject,但我试图了解如何绑定整个对象。

标签: swiftrx-swiftbehaviorsubject

解决方案


这里的关键是将每个效果视为一个单独的事物并定义导致该效果的原因。

假设您有获取和保存用户数据的方法。getUserData() -> Observable<UserData>(此 get 将发出一个值然后完成)和save(_ userData: UserData). 此外,为简单起见,我假设这些不会出错。

因此,让我们依次看一下每种效果。

  1. 填写 firstName 的初始值:
getUserData()
    .map(\.firstName)
    .bind(to: firstName.rx.text)
    .disposed(by: disposeBag)

那很简单。

  1. 填写lastName 的初始值。这有点难。我们不想订阅两次,getUserData()因为这会导致两个网络请求......share()运营商在这里提供帮助:
let userData = getUserData()
    .share()

userData
    .map(\.firstName)
    .bind(to: firstName.rx.text)
    .disposed(by: disposeBag)

userData
    .map(\.lastName)
    .bind(to: lastName.rx.text)
    .disposed(by: disposeBag)

或者如果你想稍微压缩一下:

let userData = getUserData()
    .share()

disposeBag.insert(
    userData.map(\.firstName).bind(to: firstName.rx.text),
    userData.map(\.lastName).bind(to: lastName.rx.text)
)
  1. 电子邮件是相同的:
let userData = getUserData()
    .share()

disposeBag.insert(
    userData.map(\.firstName).bind(to: firstName.rx.text),
    userData.map(\.lastName).bind(to: lastName.rx.text),
    userData.map(\.email).bind(to: email.rx.text)
)
  1. 下一个效果,save听起来像你遇到的问题。我必须在这里做更多的假设,但我猜你需要发送一个完整的、正确的用户数据对象。如果用户只是更改了他们的名字,您仍然必须在用户数据对象中发送旧的姓氏和电子邮件地址以及新的名字。

那么你从哪里得到原始数据呢?我希望这很明显,userData上面的 Observable。此外,您需要收集用户在三个字段中的任何一个中键入的任何内容。

这是扭曲...当您订阅文本字段的可观察文本时,它会发出空字符串的基值。因此,您需要跳过 Observable 发出的第一个值,并将其替换为 getUserData() 函数的输出。

let latestUserData = Observable.combineLatest(
    Observable.merge(firstName.rx.text.orEmpty.skip(1), userData.map(\.firstName)),
    Observable.merge(lastName.rx.text.orEmpty.skip(1), userData.map(\.lastName)),
    Observable.merge(email.rx.text.orEmpty.skip(1), userData.map(\.email))
) { UserData(firstName: $0, lastName: $1, email: $2) }

上面有很多代码,所以让我们稍微细想一下……对于每个字段,我都会抓取来自我们的getUserData()任何内容,并将其与来自相应文本字段的内容合并。我忽略了文本字段中出现的第一个值,因为我知道用户没有输入。

但您只想在用户点击发送按钮时调用保存函数。这就是你的触发器:

sendButton.rx.tap
    .withLatestFrom(latestUserData)
    .subscribe(onNext: { save($0) })
    .disposed(by: disposeBag)

这是一个地方的所有代码,包括observe(on:)如果您的网络 getter 在后台线程上发出其值时所必需的代码:

override func viewDidLoad() {
    super.viewDidLoad()

    let userData = getUserData()
        .observe(on: MainScheduler.instance)
        .share()

    let latestUserData = Observable.combineLatest(
        Observable.merge(firstName.rx.text.orEmpty.skip(1), userData.map(\.firstName)),
        Observable.merge(lastName.rx.text.orEmpty.skip(1), userData.map(\.lastName)),
        Observable.merge(email.rx.text.orEmpty.skip(1), userData.map(\.email))
    ) { UserData(firstName: $0, lastName: $1, email: $2) }

    sendButton.rx.tap
        .withLatestFrom(latestUserData)
        .subscribe(onNext: { save($0) })
        .disposed(by: disposeBag)

    disposeBag.insert(
        userData.map(\.firstName).bind(to: firstName.rx.text),
        userData.map(\.lastName).bind(to: lastName.rx.text),
        userData.map(\.email).bind(to: email.rx.text)
    )
}

如果你想要一个视图模型,那么你去:

func saveViewModel(trigger: Observable<Void>, firstName: Observable<String?>, lastName: Observable<String?>, email: Observable<String?>, initial: Observable<UserData>) -> Observable<UserData> {
    let latestUserData = Observable.combineLatest(
        Observable.merge(firstName.compactMap { $0 }.skip(1), initial.map(\.firstName)),
        Observable.merge(lastName.compactMap { $0 }.skip(1), initial.map(\.lastName)),
        Observable.merge(email.compactMap { $0 }.skip(1), initial.map(\.email))
    ) { UserData(firstName: $0, lastName: $1, email: $2) }

    return trigger
        .withLatestFrom(latestUserData)
}

您可以使用 RxTest 轻松测试上述内容,而无需从 UIKit 或您的网络堆栈中引入任何内容。将其输出绑定到保存函数,如下所示:

saveViewModel(
    trigger: sendButton.rx.tap.asObservable(),
    firstName: firstName.rx.text.asObservable(),
    lastName: lastName.rx.text.asObservable(),
    email: email.rx.text.asObservable(),
    initial: userData
)
    .subscribe(onNext: { save($0) })
    .disposed(by: disposeBag)

推荐阅读