3

I am building an iOS app with RxSwift and RxDataSource using VIPER architecture. I want to change the content of the UICollectionView as the value of the presenter changes (as the user typed in some letters in the searchBar, the collectionView should show all users' profile that starts with the letters), but it doesn't work as I wanted it to.

By trying debug() function, the value of presenter.section in ViewController (which holds the content of the CollectionView) is changed after I typed in some letters in the searchBar. However, the collectionView is not reflecting the change. Here are the main parts of the code.

code of ViewController

override func viewDidLoad() {

    super.viewDidLoad()
    self.view.backgroundColor = .white

    self.presenter.section
        .drive(self.searchedFriendsCollectionView.rx.items(dataSource: self.dataSource))
        .disposed(by: self.disposeBag)

    self.searchedFriendsCollectoinView.rx
        .setDelegate(self)
        .disposed(by: self.disposeBag)
}

code of Presenter

init(view: SearchFriendsViewInterface, interactor: 
SearchFriendsInteractorInterface, wireframe: 
SearchFriendsWireframeInterface) {

    self.view = view
    self.interactor = interactor
    self.wireframe = wireframe

    let allUsers = self.interactor.allUsers().asObservable()

    self.view.searchText
        .debounce(0.5)
        .filterNil()
        .distinctUntilChanged()
        .filterEmpty()
        .asObservable()
        .flatMap { text -> Observable<[SearchFriendsSection]> in
            // get all users starting with "text"
            allUsers.mapSections(query: text)
        }
        .bind(to: self.section)
        .disposed(by: self.disposeBag)
} 

extension Observable where E == [User] {

fileprivate func mapSections(query: String) -> Observable<[SearchFriendsSection]> {

    return self.map {
            $0.filter { $0.userDisplayName.hasPrefix(query) || $0.username.hasPrefix(query) }
        }
        .map { users -> [SearchFriendsSection] in

            let items = users.map { user in
                SearchFriendsItem(icon: user.profileImageURL, displayName: user.userDisplayName)
            }
            return [SearchFriendsSection(items: items)]
        }
    }
}

How I defined dataSource

override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {

    self.searchedFriendsCollectoinView = UICollectionView(frame: .init(), collectionViewLayout: self.searchedFriendsCollectionViewFlowLayout)
    let dataSource = RxCollectionViewSectionedReloadDataSource<SearchFriendsSection> (configureCell: {(_, collectionView, indexPath, item) -> UICollectionViewCell in
        collectionView.register(SearchFriendsCell.self, forCellWithReuseIdentifier: "SearchFriendsCell")
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchFriendsCell", for: indexPath) as! SearchFriendsCell
        cell.item = item
        return cell
    })
    self.dataSource = dataSource
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
  • ViewController has the instance of Presenter, and the presenter has an instance called section. This section holds the list of the users whose username is starting with specific letters.

Could you please help me? If I have left anything unclear, please let me know in the comment.

Update: output of the debug()

2019-02-01 10:22:27.610: values -> subscribed
2019-02-01 10:22:27.611: values -> Event next([])
--after typed in some letters in the searchBar--
2019-02-01 10:22:41.494: values -> Event 
next([AppScreen.SearchFriendsSection(items: 
[AppScreen.SearchFriendsItem(....), 
AppScreen.SearchFriendsItem(....), 
AppScreen.SearchFriendsItem(....)])])
Andreas ZUERCHER
  • 862
  • 1
  • 7
  • 20
joey22
  • 221
  • 1
  • 4
  • 11
  • If you put the `debug()` call just before the bind/drive/subscribe to the table view's data source itself. What is its output? – Daniel T. Jan 29 '19 at 16:00
  • Daniel T. it looks like `self.dataSource` is not changed even after I typed in some letters, it was disposed. But does the `self.dataSource` has something to do with the content of the UICollectionView? – joey22 Jan 30 '19 at 12:57
  • If the chain that drives/is bound to your dataSource is disposed, then it can't emit new values anymore. Either an Observable in your chain is sending a stop event (complete/error) or something is calling dispose() on the disposable from the bind/subscribe/drive. – Daniel T. Jan 30 '19 at 14:45
  • Sorry `self.dataSource` didn't change (because it is `let`) but when I put `debug()` right before `drive()` in the ViewController, new element was emitted. Do you have any idea why the content of the CollectionView doesn't change? – joey22 Jan 31 '19 at 15:15
  • I guess I'm not being clear... Put a `.debug("values")` in between your `self.presenter.section` line and your `.drive(self.searchedFriendsCollectionView.rx.items(dataSource: self.dataSource))` line in the view controller and post the output from that debug in your question. – Daniel T. Jan 31 '19 at 15:31
  • That's what I did. I've put the output in the question so please take a look! – joey22 Feb 01 '19 at 01:27
  • Put a breakpoint at the `collectionView.dequeueReusableCell...` line. Is it getting called? – Daniel T. Feb 01 '19 at 01:58
  • @joey22, you should officially accept the answer as the accepted answer. – Andreas ZUERCHER Apr 26 '21 at 00:45

3 Answers3

2

I found your problem. You never added the collection view to your main view. If the collection view doesn't have a super view, or it has zero size, it doesn't bother loading itself.

Put these two lines in your viewDidLoad:

searchedFriendsCollectionView.frame = view.bounds
view.addSubview(searchedFriendsCollectionView)

While you are at it, you should move this line from your configure closure into the viewDidLoad:

searchedFriendsCollectionView.register(SearchFriendsCell.self, forCellWithReuseIdentifier: "SearchFriendsCell")

There's no reason to be calling that every time the system requests a cell.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Sorry I forgot to put the code in the question, but I set CollectionView using FlexLayout. Anyway, it doesn't look like the situation has changed when I replaced the FlexLayout code with yours. When I put breakpoint in the `init(nibname)` it is called everytime I type in letters in the searchBar. Also, it looks like the value of `cell.item` is updated.... maybe the problem is in the `SearchFriendsCell` so I'll look for it. – joey22 Feb 01 '19 at 03:19
  • And yes, the problem was there. I'm sorry. Your answer helped me because I was obsessed with rxswift thing and didn't think the cause was in other parts until I see your post. Thank you so much. – joey22 Feb 01 '19 at 03:31
1

Just wondering how you define self.interactor.allUsers() and allUsers.mapSections(query: text)

Did you called onNext() to notify the observer of a new element in the sequence. after you mapped sections or getting all users?

If you really want to use flatMap. I think you should use flatMapLatest insteads

Reference: RxSwift map and flatMap difference

Updated: In this case, you need to create an new observable in mapSections which always generate [SearchFriendsSection] and send it to observer

 return Observable.create { observer in
        let filteredSections = ...
        observer.on(.next(filteredSections))
        observer.on(.completed)
        return Disposables.create()
    }
AIex H.
  • 21
  • 3
  • Thanks for commenting. I've put the code of `mapSection()` in the question, so please take a look! I think the element is notified in the sequence. `self.presenter.section` in the ViewController was everytime receiving different element when I typed in different letters as I checked using `debug()`. – joey22 Jan 30 '19 at 13:01
  • In mapSections, you just using map to transform the users emitted by allUsers. The observable you return wont emit any item.You need to create an new observable which always generate [SearchFriendsSection] and send it to observer. You can see the example in my updated answer and you better define the function in the Interactor like `findUsersBy()` if you going to do this – AIex H. Jan 30 '19 at 16:10
  • Yes it is definitely better to put method like `findUsersBy()` as you said.. Thank you. However, I think it is emitting new item. For example, if I typed nothing on the searchBar, the element is empty, and then I typed in "A" in the searchBar, element with a user named `Alex` is returned and `self.presenter.section` is receiving that according to `debug()`. Doesn't it mean the new element is emitted? To be honest I am kind of new to RxSwift so maybe it is a easy question – joey22 Jan 31 '19 at 15:11
  • It is interesting, If you can receive the items. How you put your `debug()`, are you using like this `self.presenter.section.debug().bind { _ in }.disposed(by: disposeBag)` ? However if your data stream does not have any problem, the root cause should be the `dataSource`. How you define the dataSource? – AIex H. Jan 31 '19 at 18:36
  • yes I still use that flow. I've just put the code defining the dataSource in the question, so please take a look! – joey22 Feb 01 '19 at 01:26
0

Ok, finally I solved this, and this was not directly related to Rxswift. I was using FlexLayout to layout everything, and in the CollectionViewCell, I didn't put the code below.

override func layoutSubviews() {

    super.layoutSubviews()
    self.contentView.flex.layout()
}

There was no reason the view is updated without this code because FlexLayout doesn't perform layout without this code.

joey22
  • 221
  • 1
  • 4
  • 11