RxCocoa - 防止出现滞后时推送多个视图控制器

RxCocoa - prevent multiple view controller pushes when there's lag

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

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

例如:

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

我对 RxSwift 比较陌生。我正在尝试找出一种合适的 Reactive 方法来实现此功能,以修复我的应用程序中重复推送视图的一些错误。

一些想法:

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

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

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 访问视图控制器时重新订阅。