【问题标题】:How to implement simple form with UITextFields and RxSwift?如何使用 UITextFields 和 RxSwift 实现简单的表单?
【发布时间】:2021-07-11 12:53:58
【问题描述】:

我是 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)

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

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

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

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

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

【问题讨论】:

    标签: swift rx-swift behaviorsubject


    【解决方案1】:

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

    假设您有获取和保存用户数据的方法。 getUserData() -&gt; Observable&lt;UserData&gt;(此 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: $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)
    

    【讨论】:

    • 谢谢你这么详细的回答,学到了很多关于rx的知识!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-09
    • 1970-01-01
    • 1970-01-01
    • 2014-02-08
    • 1970-01-01
    • 2016-07-27
    • 2012-12-07
    相关资源
    最近更新 更多