RxCocoa - 当有延迟时防止多个视图控制器推送

Posted

技术标签:

【中文标题】RxCocoa - 当有延迟时防止多个视图控制器推送【英文标题】:RxCocoa - prevent multiple view controller pushes when there's lag 【发布时间】:2017-09-11 20:35:13 【问题描述】:

与反应式和非反应式 ios 项目的情况一样,如果您有一个 UI 元素(例如,一个按钮或正在选择的表格视图单元格)将视图控制器推送到导航堆栈上,如果有由于某种原因(尤其是在旧设备上)滞后,重复点击可能会导致重复推送,从而导致糟糕的用户体验。

通常您可以在第一次点击后禁用该元素。

例如:

@IBAction func myButtonTap()  
    button.isEnabled = false
    doTheRestOfTheAction()

我对 RxSwift 比较陌生。我正在尝试找出一种适当的反应式方法来实现它,以修复我的应用程序中的一些错误,这些错误会导致视图被重复推送。

一些想法:

可以使用debouncethrottle,但看起来像创可贴,不一定能解决所有情况。

我目前认为最好的方法是在预期事件发生后处置订阅。

let disposable = tableView.rx.itemSelected
    .subscribe(onNext:  [weak self] indexPath in 
        self?.performSegue(withIdentifier: "MySegueIdentifier", sender: self)
    )

...

func prepareForSegue() 
    myDisposable.dispose()
    finishPrepareForSegue()

虽然如果你想在 subscribe 块内取消订阅,编译器会抱怨在它自己的初始值中使用了一个变量,这是有道理的。我想有解决方法,但我想知道,有没有更好的方法?也许我缺少一个反应式操作员?

尝试搜索类似的示例,但结果有限。

谢谢

编辑:也许是takeUntil 运营商?

Possibly relevant SO question.

【问题讨论】:

【参考方案1】:

我在公司经常看到的一件事是使用 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)
    

【讨论】:

这很有用,非常感谢你,但我不明白它是如何回答这个问题的。在某事发生后启用按钮与在某事发生后阻止未来事件不同。 @shim 您说“如果由于某种原因(尤其是在旧设备上)存在延迟,重复点击可能会导致重复推送,从而导致糟糕的用户体验。”。使用变量来切换命令的可用性将防止“重复点击”。 好吧,在我正在处理的特定情况下,我实际上并不想禁用按钮,而是停止收听单元格选择事件。我想我可以禁用表格视图,但这似乎不是正确的解决方案。 您可以“禁用”在选择单元格时调用的命令。如果您正在尝试找到一种方法来处理进入不同屏幕的两个单元格选择,那么我认为您确实需要阻止后者进入堆栈。我将在答案的底部添加一个简介,以使 commandAvailable 带有命令映射,而不是禁用按钮。 它正在调用performSegue,但我想它可以修改为调用一些中间函数,或者我可以使用shouldPerformSegue 或其他东西。【参考方案2】:

不是唯一的解决方案,但这似乎至少在推动表格视图选择的情况下运作良好。它使用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)

虽然请注意,如果您可以返回到视图控制器,那么您必须重新订阅,也许通过将订阅移动到viewDidAppear。也许有一种更有效的方法,虽然不需要重新订阅。

另一个选项是take(1) 而不是takeUntil(…),但它仍然需要在返回视图控制器时重新订阅。

【讨论】:

以上是关于RxCocoa - 当有延迟时防止多个视图控制器推送的主要内容,如果未能解决你的问题,请参考以下文章

要求 UISplitViewController 为细节视图控制器延迟加载多个视图控制器

如何防止多个子视图控制器一次加载?

如何防止子视图重叠标签栏?

设置 navigationItem 标题延迟问题

如何将多个视图控制器推送到导航控制器上

防止 viewcontroller 被推送到 splitviewcontroller 两次