ReactiveX / RxSwift

Reactive Programming in Swift
MIT License
24.38k stars 4.17k forks source link

UISearchBar.text is emptied upon cancel, but UISearchBar.rx.text is not. #1714

Closed pepasflo closed 6 years ago

pepasflo commented 6 years ago

Short description of the issue:

I created a search controller recently and noticed that when the user taps "cancel", the search text gets emptied out, but this isn't reflected via UISearchBar.rx.text.

Expected outcome:

Here's what I ended up with as a work-around, which I think describes the behavior I was expecting:

Observable.combineLatest(
    self.searchBar.rx.text,
    self.searchBar.rx.textDidEndEditing.startWith(())
)
    .map { _ in return self.searchBar.text ?? "" }
    .distinctUntilChanged()
    .subscribe(onNext: { (text) in
        print("searchbar text: \(text)")
    })
    .disposed(by: self.disposeBag)

What actually happens:

When the user hits "cancel", the text is visually gone from the search bar. I was a bit surprised that this update isn't reflected in rx.cancelButtonClicked, but the text update is reflected by the time .rx.textDidEndEditing gets hit.

Self contained code example that reproduces the issue:

self.searchBar.rx.text
    .subscribe(onNext: { (text) in
        print("searchBar.rx.text.onNext: \(String(describing: text))")
    })
    .disposed(by: self.disposeBag)

self.searchBar.rx.cancelButtonClicked
    .subscribe(onNext: { () in
        print("searchBar.rx.cancelButtonClicked.onNext: text: \(String(describing: self.searchBar.text))")
    })
    .disposed(by: self.disposeBag)

self.searchBar.rx.textDidEndEditing
    .subscribe(onNext: { () in
        print("searchBar.rx.textDidEndEditing.onNext: text: \(String(describing: self.searchBar.text))")
    })
    .disposed(by: self.disposeBag)

Observable.combineLatest(
    self.searchBar.rx.text,
    self.searchBar.rx.textDidEndEditing.startWith(())
)
    .map { _ in return self.searchBar.text ?? "" }
    .distinctUntilChanged()
    .subscribe(onNext: { (text) in
        print("combineLatest: text: \(text)")
    })
    .disposed(by: self.disposeBag)

RxSwift/RxCocoa/RxBlocking/RxTest version/commit

$ cat Podfile.lock 
PODS:
  - RxCocoa (4.2.0):
    - RxSwift (~> 4.0)
  - RxSwift (4.2.0)
  - RxSwiftExt (3.2.0):
    - RxSwiftExt/Core (= 3.2.0)
  - RxSwiftExt/Core (3.2.0):
    - RxSwift (~> 4.0)

Platform/Environment

How easy is to reproduce? (chances of successful reproduce after running the self contained code)

Xcode version:

9.4.1

:warning: Fields below are optional for general issues or in case those questions aren't related to your issue, but filling them out will increase the chances of getting your issue resolved. :warning:

Installation method:

I have multiple versions of Xcode installed: (so we can know if this is a potential cause of your issue)

Level of RxSwift knowledge: (this is so we can understand your level of knowledge and formulate the response in an appropriate manner)

kzaher commented 6 years ago

This sounds like a potential bug to me, I think we might want to add self.searchBar.rx.textDidEndEditing or self.searchBar.rx.didCancel to the text trigger if that is a problem.

freak4pc commented 6 years ago

@pepasflo

From my quick test I don't see any issues.

Tapping the cancel button triggers cancelButtonClicked (by the way would be better to call this tapped, no biggie) Tapping the clear button clears the textfields, and properly emits a "" text from rx.text.

Are you sure you're not confused with "clear" vs. "cancel" buttons?

image

The last "" emission is after I tap the clear button.

pepasflo commented 6 years ago

@freak4pc here is a more complete example:

import UIKit
import RxSwift
import RxCocoa

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        let window = UIWindow(frame: UIScreen.main.bounds)
        window.makeKeyAndVisible()
        window.rootViewController = UINavigationController(rootViewController: ViewController())
        self.window = window

        return true
    }
}

class ViewController: UIViewController {

    private let searchController: UISearchController = {
        let controller = UISearchController(searchResultsController: nil)
        controller.dimsBackgroundDuringPresentation = false
        return controller
    }()

    private var searchBar: UISearchBar {
        return self.searchController.searchBar
    }

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white

        self.navigationItem.searchController = self.searchController

        let observable1 = self.searchBar.rx.text

        let observable2 = Observable.combineLatest(
            self.searchBar.rx.text,
            self.searchBar.rx.textDidEndEditing.startWith(())
        )

        observable1
            .debug("debug:", trimOutput: false)
            .takeUntil(self.rx.deallocated)
            .map { [unowned self] _ in return self.searchBar.text ?? "" }
            .distinctUntilChanged()
            .subscribe(onNext: { (text) in
                print("onNext: '\(text)'")
            })
            .disposed(by: self.disposeBag)

    }
}

In the above example, if I tap in the search box, then type "A", the following is printed to the console:

2018-08-15 12:50:53.546: debug: -> Event next(Optional("A"))
onNext: 'A'

If I hit the "Cancel" button, nothing is printed to the console (but visually, the text is gone from the search bar).

If we modify the above example so that we subscribe to observable2 rather than observable1, and I tap into the search bar and tap 'A', I see this:

2018-08-15 12:53:02.268: debug: -> Event next((Optional("A"), ()))
onNext: 'A'

If I then hit "Cancel", I see this:

2018-08-15 12:53:08.139: debug: -> Event next((Optional("A"), ()))
onNext: ''
freak4pc commented 6 years ago

@pepasflo Mind making a self-reproducible example to make things easier to just run and try? :) Highly appreciate it.

reteps commented 6 years ago

This issue still exists. .text does not emit anything after the cancel button is clicked. My current workaround:

let cancelTextObservable = searchController.searchBar.rx.textDidEndEditing.map({ _ in
    return ""
})
let searchTextObservable = searchController.searchBar.rx.text.orEmpty.asObservable()
Observable.of(cancelTextObservable, searchTextObservable).merge()
pepasflo commented 6 years ago

@kzaher was this issue fixed, or was it closed with a recommendation for the above work-around?

(don't want this comment to sound critical -- I am very grateful for your work on RxSwift! πŸ‘)

kzaher commented 6 years ago

It was fixed in the latest release.

kzaher commented 6 years ago

There is a commit that references this issue just above.

pepasflo commented 6 years ago

😍😍😍

alexpersian commented 5 years ago

This is still reproducible with the 4.4 release.

searchController.searchBar.rx.text.orEmpty

Using this style of binding code produces the issue where Cancel does not fire an event for the empty string. Converting to the provided workaround:

Observable.combineLatest(
    searchController.searchBar.rx.text,
    searchController.searchBar.rx.textDidEndEditing.startWith(())
)

results in proper behavior.

thegodland commented 3 years ago

in my code, if I called self.searchBar.endEditing(true) (behavior like click search button on keyboard) before click the cancel button, then it won't work... wonder how to solve in this case?