Reputation: 33
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?
Upvotes: 0
Views: 110
Reputation: 437872
With asynchronous calls to fetch user data, the fact that cells are re-used introduces two issues:
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.
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.
Upvotes: 1