Swift - InstantSearch - collectionView... numberOfItemsInSection called multiple times

Hello everyone,

I am using InstantSearch to fetch data and display them in a collection view. To do that, I am using the code below:

class ProfileView: UIView, HitsController {
var singleIndexHitsInteractor: HitsInteractor = .init(showItemsOnEmptyQuery: true )
var challengeCollectionView: UICollectionView?
public weak var hitsSource: HitsInteractor?

private func setupChallengeIndexSearchSettings() {
singleIndexHitsInteractor.connectSearcher(challengesIndexSearcher)
}

private func searchCreatedChallenges() {
    var filterState = FilterState()
    filterState = setupFilter(indexName: "challenges", challengeState: SELECTED_CHALLENGE_STATE)

    print(filterState.getFilters())
    challengesIndexSearcher.connectFilterState(filterState)
    challengesIndexSearcher.query = searchBarController.searchBar.text
    challengesIndexSearcher.search()
}


 func scrollToTop() {
    if (hitsSource?.numberOfHits() == 0) {
        return
    }
    self.challengeCollectionView?.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
// FUNCTION BELOW IS CALLED MULTIPLE TIMES AFTER CELLFORROW..
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    if (hitsSource?.numberOfHits() == 0) {
        addNoChallengesLabel()
        return 0
    }
    else {
        removeNoChallengesLabel()
        return (hitsSource?.numberOfHits())!
    }
}
 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let myChallengesCell = collectionView.dequeueReusableCell(withReuseIdentifier: "myChallenges", for: indexPath) as! MyChallengesCollectionViewCell

    if (challengeStateSegmentedControl.selectedSegmentIndex == 0) {
        let challengeObject = Challenge(json: (hitsSource?.hit(atIndex: indexPath.row))!)
        print(challengeObject.id)
       
        myChallengesCell.setChallengeTitle(challengeObject:challengeObject)
       ...

Using the code above, I get the results I want. First

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int)

is called and then

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

But what I don’t understand - why is numberOfItemsInSection called again after going through three times (as expected) within cellForItemAt? I mean, in the next step, it is going again three times but I thought since we have the datasource set, there is no reason call that function as long as you don’t make a refresh?

I observed following in the logs
"The request timed out." UserInfo={NSUnderlyingError=0x280c3f2a0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=https://XXX-dsn.algolia.net/1/indexes/*/queries,**

Not sure why a timeout happens. I get the objects anyway but could it be that setting the source is called multiple times because of a retry-mechanism?

Thanks in advance!
Take care and stay healthy

Ok, setting a breakpoint in the method

func reload() {
   challengeCollectionView?.reloadData()
}

Show that it is called multiple times. I had to implement the function above because of using HitsController. Can anyone tell me why it is called multiple times?

Hello guys, does anyone have an idea? Thanks

Hello @kaan548,

Multiple reasons may lead to extra reload() method calls.

Most likely this happens due to the infinite scrolling implementation. Depending on the pagination settings, the HitsInteractor may fetch the additional hits pages to ensure the smooth infinite scrolling experience. The reception of each new hits page causes the reload of the hits controller. There is a room for optimization here, e.g. we can call reload() method only in case of changes of the visible hits, but that would significantly complicate the HitsController protocol complexity, something we prefer to avoid.

Another reason could be the redundant connections between your HitsInteractor and HitsController. Make sure that HitsInteractor.connectController method is called just once.

1 Like

Highly appreciate your answers @vladislav.fitc and algolia-team :slight_smile:
However, the reason why I am calling HitsInteractor.connectController multiple times is, I have one collection view which should display objects from different indices - depending on the filter of the app user.

When I tried to change the searcher just by using singleIndexHitsInteractor.connectSearcher(indexName) and then doing the actual search with myChallengesIndexSearcher.search(), it still gave me the results of the previous index. What do I need to do in a proper way for being able to switch between the indices?

Thanks!

You can switch the Index by setting singleIndexSearcher.indexQueryState.indexName property. This will properly work with connected InstantSearch components, if the type of the hits in both indices is the same.

If you prefer to switch connections manually, each connect method (connectSearcher, connectController etc.) returns a Connection structure providing connect() and disconnect() methods. Connect method is called automatically when you connect components, but disconnect must be called manually. By doing this you will avoid redundant connections and thus redundant table/collection view reloads.

1 Like

Thanks @vladislav.fitc. I will try it out. Take care!

Hello @vladislav.fitc,

I hope, you are doing fine. Actually, I am struggling with this issue now. I try to disconnect the current connection and then manually connect with another index, however, I still get the results of the previous connection.

Here is my code:

class ProfileView: UIView, HitsController {
let myChallengesIndexSearcher = SingleIndexSearcher(appID: “XX”, apiKey: “XX”, indexName: “challenges”)
let myParticipationIndexSearcher = SingleIndexSearcher(appID: “XX”, apiKey: “XX”, indexName: “participation”)
let searchBarController: SearchBarController = .init(searchBar: UISearchBar())
// OBJECTS TO STORE CONNECTION
var myChallengesIndexConnection:HitsInteractor.SingleIndexSearcherConnection?
var participationIndexConnection:HitsInteractor.SingleIndexSearcherConnection?
var winningIndexConnection:HitsInteractor.SingleIndexSearcherConnection?
var ratingIndexConnection:HitsInteractor.SingleIndexSearcherConnection?

This is the way, how I assign connection to the variables above

private func setupChallengeIndexSearchSettings() {
myChallengesIndexConnection = 
singleIndexHitsInteractor.connectSearcher(myChallengesIndexSearcher) }

private func setupWinningIndexSearchSettings() {
    winningIndexConnection = singleIndexHitsInteractor.connectSearcher(winningIndexSearcher)
}

private func setupParticipationIndexSearchSettings() {
   participationIndexConnection = singleIndexHitsInteractor.connectSearcher(myParticipationIndexSearcher)
}

private func setupRatingIndexSearchSettings() {
    ratingIndexConnection = singleIndexHitsInteractor.connectSearcher(ratingIndexSearcher)
}

Everytime, when I want switch between the indices, I disconnect them:

 private func disconnect() {
    myChallengesIndexConnection?.disconnect()
    participationIndexConnection?.disconnect()
    winningIndexConnection?.disconnect()
    ratingIndexConnection?.disconnect()
}

Well, I disconnect everything even I would only have to disconnect one connection but should not hurt or?

private func searchParticipations() {
    var filterState = FilterState()
    filterState = setupFilter(indexName: "participation", challengeState: SELECTED_CHALLENGE_STATE)
      
    myParticipationIndexSearcher.connectFilterState(filterState)
    myParticipationIndexSearcher.query = searchBarController.searchBar.text
    myParticipationIndexSearcher.search()
    myParticipationIndexSearcher.connectFilterState(filterState).disconnect() // ok, like this?
}

When doing myParticipationIndexSearcher.connectFilterState(filterState), do I get a new connection everytime? If not, I probably don’t need it the variables (storing the connection) above or?

Could you also tell me, what I am doing wrong? What don’t I get the correct response even if the correct search is triggered?

Thank you!

Hi @kaan548,

I’m not sure what are you trying to achieve, but if you just need to switch the index name, you can do it using one SingleIndexSearcher by setting

let searcher: SingleIndexSearcher = ...
searcher.indexQueryState.indexName = "MyIndexName"
searcher.indexQueryState.query.page = 0
searcher.search()

In this case one connection between SingleIndexSearcher and HitsInteractor is enough.
But this will work only if your indices contain the records in the same JSON format.
Otherwise, you have to add one HitsInteractor per index parametrized with an appropriate hit model.
You can subscribe to onError event of SingleIndexSearcher and HitsInteractor to catch the errors and to define what’s going wrong.

myParticipationIndexSearcher.connectFilterState(filterState).disconnect() // ok, like this?

This is not ok. Here you establish a new connection and destroy it right away. The previous approach with storing a connection in the optional variable is correct.