如何用 UITextFields 和 RxSwift 实现简单的表单?

How to implement simple form with UITextFields and RxSwift?

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

简化示例:

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

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: [=14=],
        lastName: ,
        email: 
    )
}
.bind(to: viewModel.userData)
.disposed(by: disposeBag)

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

为了发送数据我使用了一个简单的tap handler,感觉不是很Rx:

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

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

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

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

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

那么让我们依次看看每个效果。

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

嗯,这很简单。

  1. 正在填写姓氏的初始值。这个有点难。我们不想订阅两次 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: [=14=], lastName: , email: ) }

上面有很多代码,所以让我们仔细研究一下...对于每个字段,我都抓取来自 getUserData() 的所有内容并将其与来自 getUserData() 的任何内容合并适当的文本字段。不过,我忽略了文本字段中出现的第一个值,因为我知道用户没有输入它。

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

sendButton.rx.tap
    .withLatestFrom(latestUserData)
    .subscribe(onNext: { save([=15=]) })
    .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: [=16=], lastName: , email: ) }

    sendButton.rx.tap
        .withLatestFrom(latestUserData)
        .subscribe(onNext: { save([=16=]) })
        .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 { [=17=] }.skip(1), initial.map(\.firstName)),
        Observable.merge(lastName.compactMap { [=17=] }.skip(1), initial.map(\.lastName)),
        Observable.merge(email.compactMap { [=17=] }.skip(1), initial.map(\.email))
    ) { UserData(firstName: [=17=], lastName: , email: ) }

    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([=18=]) })
    .disposed(by: disposeBag)