RxCocoa - 防止出现滞后时推送多个视图控制器
RxCocoa - prevent multiple view controller pushes when there's lag
与响应式和非响应式 iOS 项目一样,如果您有一个 UI 元素(例如,一个按钮或一个 table 视图单元格选中)将视图控制器推送到导航堆栈,如果由于某种原因(尤其是在旧设备上)出现延迟,重复点击可能会导致重复推送,从而导致糟糕的用户体验。
通常您可以在第一次点击后禁用该元素。
例如:
@IBAction func myButtonTap() {
button.isEnabled = false
doTheRestOfTheAction()
}
我对 RxSwift 比较陌生。我正在尝试找出一种合适的 Reactive 方法来实现此功能,以修复我的应用程序中重复推送视图的一些错误。
一些想法:
可以使用 debounce
或 throttle
,但看起来像是创可贴,不一定能解决所有问题。
我目前认为最好的方法是在预期事件发生后处理订阅。
let disposable = tableView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
self?.performSegue(withIdentifier: "MySegueIdentifier", sender: self)
})
...
func prepareForSegue() {
myDisposable.dispose()
finishPrepareForSegue()
}
尽管如果您想在订阅块内取消订阅,编译器会抱怨在其自身初始值内使用变量,这是有道理的。我想有解决方法,但我想知道,有没有更好的方法?也许我缺少一个 Reactive 运算符?
尝试搜索类似示例,但结果有限。
谢谢
编辑:也许是 takeUntil
运算符?
.
我在公司经常看到的一件事是使用 Rx 的 Variable
,它类似于 loginInFlight
,即 Variable<boolean>
。这默认为 false,当命令为 运行 登录时,我们将其翻转为 true。这个布尔值也与登录按钮相关联,因此一旦用户点击登录,任何后续点击都不会执行任何操作。您可以在用户可以单击某些内容以更改屏幕的任何地方实施此操作,以确保没有正在进行的呼叫/事件。
我们遵循 MVVM,所以这里有一个基于它的示例。我试着只展示它下面的准系统,所以希望下面的一切仍然有意义。
登录视图控制器
class LoginViewController: UIViewController {
@IBOutlet weak var signInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
...
// This commandAvailable is what I was talking about above
viewModel?
.loginCommandAvailable
.subscribe(onNext: {[unowned self] (available: Bool) in
self.signInButton.isEnabled = available
})
.addDisposableTo(disposeBag)
signInButton.rx.tap
.map {
// Send Login Command
return viewModel?.loginCommand()
}.subscribe(onNext: { (result: LoginResult)
// If result was successful we can send the user to the next screen
}).addDisposableTo(disposeBag)
}
}
登录视图模型
enum LoginResult: Error {
case success
case failure
}
class LoginViewModel {
private let loginInFlight = Variable<Bool>(false)
private var emailAddressProperty = Variable<String>("")
var emailAddress: Driver<String> {
return emailAddressProperty
.asObservable()
.subscribeOn(ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()))
.asDriver(onErrorJustReturn: "")
}
...
var loginCommandAvailable: Observable<Bool> {
// We let the user login if login is not currently happening AND the user entered their email address
return Observable.combineLatest(emailAddressProperty.asObservable(), passwordProperty.asObservable(), loginInFlight.asObservable()) {
(emailAddress: String, password: String, loginInFlight: Bool) in
return !emailAddress.isEmpty && !password.isEmpty && !loginInFlight
}
}
func loginCommand() -> Driver<LoginResult> {
loginInFlight.value = true
// Make call to login
return authenticationService.login(email: emailAddressProperty.value, password: passwordProperty.value)
.map { result -> LoginResult in
loginInFlight.value = false
return LoginResult.success
}
}
}
根据可用性编辑切换命令
登录视图控制器
class LoginViewController: UIViewController {
@IBOutlet weak var signInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
...
// This commandAvailable is what I was talking about above
viewModel?
.loginCommandAvailable
.subscribe(onNext: {[unowned self] (available: Bool) in
self.signInButton.isEnabled = available
})
.addDisposableTo(disposeBag)
signInButton.rx.tap
.map {
return viewModel?.loginCommandAvailable
}.flatmap { (available: Bool) -> Observable<LoginResult>
// Send Login Command if available
if (available) {
return viewModel?.loginCommand()
}
}.subscribe(onNext: { (result: LoginResult)
// If result was successful we can send the user to the next screen
}).addDisposableTo(disposeBag)
}
}
不是唯一的解决方案,但这似乎至少在推动 table 视图选择的情况下效果很好。它使用 takeUntil
运算符来停止事件
myTableView.rx.itemSelected
.takeUntil(self.rx.methodInvoked(#selector(viewWillDisappear)))
.subscribe(onNext: { [weak self] indexPath in
self?.performSegue(withIdentifier: "MySegueIdentifier", sender: self)
)}
.dispose(by: self.myDisposeBag)
尽管请注意,如果您可以 return 查看控制器,则您必须重新订阅,也许可以将订阅移至 viewDidAppear
。也许有一个更有效的方法,虽然不需要重新订阅。
另一个选项是 take(1)
而不是 takeUntil(…)
,但它仍然需要在 return 访问视图控制器时重新订阅。
与响应式和非响应式 iOS 项目一样,如果您有一个 UI 元素(例如,一个按钮或一个 table 视图单元格选中)将视图控制器推送到导航堆栈,如果由于某种原因(尤其是在旧设备上)出现延迟,重复点击可能会导致重复推送,从而导致糟糕的用户体验。
通常您可以在第一次点击后禁用该元素。
例如:
@IBAction func myButtonTap() {
button.isEnabled = false
doTheRestOfTheAction()
}
我对 RxSwift 比较陌生。我正在尝试找出一种合适的 Reactive 方法来实现此功能,以修复我的应用程序中重复推送视图的一些错误。
一些想法:
可以使用 debounce
或 throttle
,但看起来像是创可贴,不一定能解决所有问题。
我目前认为最好的方法是在预期事件发生后处理订阅。
let disposable = tableView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
self?.performSegue(withIdentifier: "MySegueIdentifier", sender: self)
})
...
func prepareForSegue() {
myDisposable.dispose()
finishPrepareForSegue()
}
尽管如果您想在订阅块内取消订阅,编译器会抱怨在其自身初始值内使用变量,这是有道理的。我想有解决方法,但我想知道,有没有更好的方法?也许我缺少一个 Reactive 运算符?
尝试搜索类似示例,但结果有限。
谢谢
编辑:也许是 takeUntil
运算符?
我在公司经常看到的一件事是使用 Rx 的 Variable
,它类似于 loginInFlight
,即 Variable<boolean>
。这默认为 false,当命令为 运行 登录时,我们将其翻转为 true。这个布尔值也与登录按钮相关联,因此一旦用户点击登录,任何后续点击都不会执行任何操作。您可以在用户可以单击某些内容以更改屏幕的任何地方实施此操作,以确保没有正在进行的呼叫/事件。
我们遵循 MVVM,所以这里有一个基于它的示例。我试着只展示它下面的准系统,所以希望下面的一切仍然有意义。
登录视图控制器
class LoginViewController: UIViewController {
@IBOutlet weak var signInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
...
// This commandAvailable is what I was talking about above
viewModel?
.loginCommandAvailable
.subscribe(onNext: {[unowned self] (available: Bool) in
self.signInButton.isEnabled = available
})
.addDisposableTo(disposeBag)
signInButton.rx.tap
.map {
// Send Login Command
return viewModel?.loginCommand()
}.subscribe(onNext: { (result: LoginResult)
// If result was successful we can send the user to the next screen
}).addDisposableTo(disposeBag)
}
}
登录视图模型
enum LoginResult: Error {
case success
case failure
}
class LoginViewModel {
private let loginInFlight = Variable<Bool>(false)
private var emailAddressProperty = Variable<String>("")
var emailAddress: Driver<String> {
return emailAddressProperty
.asObservable()
.subscribeOn(ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()))
.asDriver(onErrorJustReturn: "")
}
...
var loginCommandAvailable: Observable<Bool> {
// We let the user login if login is not currently happening AND the user entered their email address
return Observable.combineLatest(emailAddressProperty.asObservable(), passwordProperty.asObservable(), loginInFlight.asObservable()) {
(emailAddress: String, password: String, loginInFlight: Bool) in
return !emailAddress.isEmpty && !password.isEmpty && !loginInFlight
}
}
func loginCommand() -> Driver<LoginResult> {
loginInFlight.value = true
// Make call to login
return authenticationService.login(email: emailAddressProperty.value, password: passwordProperty.value)
.map { result -> LoginResult in
loginInFlight.value = false
return LoginResult.success
}
}
}
根据可用性编辑切换命令
登录视图控制器
class LoginViewController: UIViewController {
@IBOutlet weak var signInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
...
// This commandAvailable is what I was talking about above
viewModel?
.loginCommandAvailable
.subscribe(onNext: {[unowned self] (available: Bool) in
self.signInButton.isEnabled = available
})
.addDisposableTo(disposeBag)
signInButton.rx.tap
.map {
return viewModel?.loginCommandAvailable
}.flatmap { (available: Bool) -> Observable<LoginResult>
// Send Login Command if available
if (available) {
return viewModel?.loginCommand()
}
}.subscribe(onNext: { (result: LoginResult)
// If result was successful we can send the user to the next screen
}).addDisposableTo(disposeBag)
}
}
不是唯一的解决方案,但这似乎至少在推动 table 视图选择的情况下效果很好。它使用 takeUntil
运算符来停止事件
myTableView.rx.itemSelected
.takeUntil(self.rx.methodInvoked(#selector(viewWillDisappear)))
.subscribe(onNext: { [weak self] indexPath in
self?.performSegue(withIdentifier: "MySegueIdentifier", sender: self)
)}
.dispose(by: self.myDisposeBag)
尽管请注意,如果您可以 return 查看控制器,则您必须重新订阅,也许可以将订阅移至 viewDidAppear
。也许有一个更有效的方法,虽然不需要重新订阅。
另一个选项是 take(1)
而不是 takeUntil(…)
,但它仍然需要在 return 访问视图控制器时重新订阅。