Swift异步闭包问题:UICollectionView数据源加载异常
Hey there, let's break down exactly what's going wrong with your code and get that collectionView loading data properly!
The Core Problems
You're running into three key issues that are blocking your data from showing up:
Asynchronous Execution Timing
In yourviewDidLoad, you setself.datasource = dataand callcollectionView?.reloadData()before yourgetPrivatePostsclosure finishes fetching data. Since Firebase calls are async, your collectionView refreshes whendata.postsis still empty. Even after you assigndata.posts = posts!in the closure, you never tell the collectionView to refresh again.Firebase Listener Misuse
You're usingobserve(.value)which sets up a persistent listener that triggers every time the data changes. For initial data loading, you should useobserveSingleEvent(of: .value)to fetch the data once (unless you want real-time updates, which isn't indicated here).Premature Completion Calls
Yourcompletion(ret)is called inside the loop over each contact's privatePosts. This means you're returning partial data multiple times instead of waiting until all contacts' posts are collected.
The Fixed Code
Let's rewrite your code step by step to fix these issues:
1. Update Your homeView Class
First, move the homeDataSource instance to a class property so it's accessible everywhere, and fix the async flow:
import LBTAComponents import Firebase class homeView: DatasourceController { // Make data a class property instead of local to viewDidLoad let data = homeDataSource() override func viewDidLoad() { super.viewDidLoad() self.datasource = data getPrivatePosts { [weak self] posts in guard let self = self, let posts = posts else { return } // Assign posts to datasource on the MAIN THREAD (UI updates must be on main) DispatchQueue.main.async { self.data.posts = posts print("Final data.posts count: \(self.data.posts.count)") self.collectionView?.reloadData() } } } // Keep your existing collectionView layout methods here override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: view.frame.width , height: 150) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: view.frame.width, height: 0 ) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return CGSize(width: view.frame.width, height: 0) } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { performSegue(withIdentifier: "goToNewPost", sender: self) }
2. Fix the getPrivatePosts Function
We'll use a counter to wait for all contact posts to load, switch to single-event listeners, and only call completion once all data is ready:
func getPrivatePosts(completion : @escaping (_ privatePosts : [Post]?) ->()){ var ret = [Post]() var pendingRequests = 0 guard let currentUid = Auth.auth().currentUser?.uid else { completion(nil) return } staticValuesForData.instance.dataBaseUserref.child(currentUid).child("contacts").observeSingleEvent(of: .value) { snapshot in let contactSnapshots = snapshot.children.allObjects as! [DataSnapshot] pendingRequests = contactSnapshots.count // If no contacts, return empty array immediately if pendingRequests == 0 { completion(ret) return } for contactSnapshot in contactSnapshots { guard let contactUid = contactSnapshot.childSnapshot(forPath: "uid").value as? String else { pendingRequests -= 1 if pendingRequests == 0 { completion(ret) } continue } staticValuesForData.instance.dataBaseUserref.child(contactUid).child("privatePosts").observeSingleEvent(of: .value) { postSnapshot in defer { // Decrement counter after processing this contact's posts pendingRequests -= 1 // If all requests are done, return the full array if pendingRequests == 0 { completion(ret) } } let posts = postSnapshot.children.allObjects as! [DataSnapshot] for post in posts { guard let dict = post.value as? [String : AnyObject] else { continue } // Parse post data let fullname = dict["fullname"] as! String let picUrl = dict["picUrl"] as! String let postContent = dict["postContent"] as! String let time = dict["time"] as! Int let uid = dict["uid"] as! String let username = dict["username"] as! String let reposts = dict["reposts"] as! [String] let downs = dict["downs"] as! [String] // Parse comments var comments = [Comment]() let commentArr = postSnapshot.childSnapshot(forPath: "comments").children.allObjects as! [DataSnapshot] for c in commentArr { guard let commentDict = c.value as? [String : AnyObject] else { continue } let cuid = commentDict["uid"] as! String let ccommentText = commentDict["commentText"] as! String let cpicUrl = commentDict["picUrl"] as! String let cusername = commentDict["username"] as! String let ctime = commentDict["time"] as! Int let com = Comment(uid: cuid, commentText: ccommentText, time: ctime, picUrl: cpicUrl, username: cusername) comments.append(com) } // Create post object and add to array let postToAdd = Post(postContent: postContent, picUrl: picUrl, userName: username, fullName: fullname, postID: uid, postTime: time, downs: downs, reposts: reposts, comments: comments) ret.append(postToAdd) } } } } } }
3. homeDataSource Class (No Changes Needed)
Your existing homeDataSource is fine, but just a reminder: make sure your userCell correctly uses the Post object from item(_ indexPath:) to populate its UI.
Key Improvements Explained
- Class-level Datasource: Moving
datato a class property ensures it's retained and accessible throughout the view controller's lifecycle. - Main Thread UI Updates: Wrapping
reloadData()inDispatchQueue.main.asyncguarantees UI changes happen on the main thread (required for all UIKit operations). - Single-Event Listeners:
observeSingleEventfetches data once instead of setting up a permanent listener, which is better for initial loading. - Pending Request Counter: We track how many contact post requests are pending, and only call completion when all are done, ensuring we return the full set of posts.
- Weak Self Capture: Using
[weak self]in the closure prevents retain cycles (a common memory leak issue in iOS).
内容的提问来源于stack exchange,提问作者zackkkkkkk

