简体   繁体   中英

Swift - Firebase - Downloading images and data asynchronously leads to wrong display within the collection view cell

I have a collection view and want to load images and other data asynchronously from firebase and display them within the cell. However, my current approach displays wrong images to the text data (they simply don't fit) and also, the image in the one specific cell changes few times until it settles down (sometime wrong, sometimes correct).

My code

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
            
    // issue when refreshing collection view after a new challenge has been created
    if (hitsSource?.hit(atIndex: indexPath.row) == nil)  {
        return photoCell
    }
    
    let challengeObject = Challenge(json: (hitsSource?.hit(atIndex: indexPath.row))!)

    let group = DispatchGroup()
    group.enter()

    // async call
    self.checkIfChallengeIsBlocked(completionHandler: { (IsUserBlocked) in
        if (IsUserBlocked) {
            return
        }
        else {
            group.leave()
        }
            
    }, challengeObject: challengeObject)
    
    group.notify(queue: .main) {
        photoCell.setChallengeLabel(title: challengeObject.title)
        // async call
        photoCell.fetchChallengeImageById(challengeObject: challengeObject)
                                  
        photoCell.checkIfToAddOrRemovePlayIcon(challengeObject: challengeObject)
        // async call   
        self.dataAccessService.fetchUserById(completionHandler: { (userObject) in
            photoCell.setFullName(userObject: userObject)
            photoCell.setStarNumber(challengeObject: challengeObject)
        }, uid: challengeObject.organizerId)
           
         // async all 
        self.dataAccessService.fetchAllParticipantsByChallengeId(completionHandler: { (participationArray) in
            photoCell.setParticipationNumber(challengeObject: challengeObject, participationArray: participationArray)
        }, challengeId: challengeObject.id)
            
        // resize image to collection view cell
        self.activityView.removeFromSuperview()
    }
    
    return photoCell
}

... Just to show you my MainViewCollectionViewCell

class MainViewCollectionViewCell: UICollectionViewCell  {
...
public func fetchChallengeImageById(challengeObject:Challenge) {
    self.dataAccessService.fetchChallengeImageById(completion: { (challengeImage) in
        self.challengeImageView.image = challengeImage
        self.layoutSubviews()
    }, challengeId: challengeObject.id)
} 

and DataAccessService.swift

class DataAccessService {
 ...
 // fetch main challenge image by using challenge id
public func fetchChallengeImageById(completion:@escaping(UIImage)->(), challengeId:String) { 
 //throws {
    BASE_STORAGE_URL.child(challengeId).child(IMAGE_NAME).getData(maxSize: 1 * 2048 * 2048, 
 completion:({ data, error in
        if error != nil {
            print(error?.localizedDescription as Any)
            let notFoundImage = UIImage()
            completion(notFoundImage)
        } else {
            let image = UIImage(data: data!)!
            completion(image)
        }
    }))
}

...

public func fetchUserById(completionHandler:@escaping(_ user: User)->(), uid:String?) { // 
throws{
    var userObject = User()
    let _userId = UserUtil.validateUserId(userId: uid)
    USER_COLLECTION?.whereField("uid", isEqualTo: _userId).getDocuments(completion: { 
 (querySnapshot, error) in
        
        if error != nil {
            self.error = error
            print(error?.localizedDescription as Any)
        } else {
            for document in querySnapshot!.documents {
                userObject = User(snapShot: document)
                completionHandler(userObject)
            }
        }
    })
}

Could anyone tell me what I need to change for being able to fit the text data to the correct image in the cell?

With asynchronous calls to fetch user data, the fact that cells are re-used introduces two issues:

  1. When a cell is re-used, make sure that you do not show the values for the prior cell while your asynchronous request is in progress. Either have collectionView(_:cellForItemAt:) reset the values or, better, have the cell's prepareForReuse make sure the controls are reset.

  2. In the asynchronous request completion handler, check to see if the cell is still visible before updating it. You do this by calling collectionView.cellForItem(at:) . If the resulting cell is nil , then the cell is not visible and there's nothing to update.

Thus:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell

    // make sure to initialize these so if the cell has been reused, you don't see the old values

    photoCell.label.text = nil
    photoCell.imageView.image = nil

    // now in your asynchronous process completion handler, check to make sure the cell is still visible

    someAsynchronousProcess(for: indexPath.row) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }

        // update `cell`, not `photoCell` in here
    }

    return photoCell
}

Obviously, if one asynchronous completion handler initiates another asynchronous request, then you have to repeat this pattern.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM