如何在Firebase Swift中实现用户间好友关系?附DataService.swift文件
Hey there! Let's walk through how to build a solid friend relationship system in Firebase for your Swift app, integrating it right into your existing DataService.swift. We'll cover data structure design, core functionality, real-time updates, and security rules to keep things safe.
1. Firebase Data Structure Design
First, we'll use a scalable Firestore structure (adjust if you're using Realtime Database) that supports bidirectional relationships and easy querying:
// Firestore Collections - friendRequests - [senderUserId] - sentRequests - [receiverUserId]: { timestamp: Timestamp, status: "pending" } - [receiverUserId] - receivedRequests - [senderUserId]: { timestamp: Timestamp, status: "pending" } - friends - [userId] - userFriends - [friendUserId]: { addedAt: Timestamp } - users (your existing user collection, storing profiles)
Storing friendships bidirectionally means each user has their own list of friends, making it fast to fetch a user's friends without complex queries.
2. Core Functions for DataService.swift
Assuming your DataService already has a Firestore reference (let db = Firestore.firestore()), add these methods to handle friend interactions:
Send a Friend Request
This method checks for existing friendships/requests first, then sends the request atomically using a batch write:
func sendFriendRequest(to receiverId: String, from senderId: String, completion: @escaping (Error?) -> Void) { // Check if already friends let friendCheckRef = db.collection("friends").document(senderId).collection("userFriends").document(receiverId) friendCheckRef.getDocument { snapshot, error in if let error = error { completion(error) return } if snapshot?.exists == true { completion(NSError(domain: "FriendError", code: 0, userInfo: [NSLocalizedDescriptionKey: "You're already friends!"])) return } // Check if request already sent let sentRequestRef = db.collection("friendRequests").document(senderId).collection("sentRequests").document(receiverId) sentRequestRef.getDocument { snapshot, error in if let error = error { completion(error) return } if snapshot?.exists == true { completion(NSError(domain: "FriendError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Request already sent!"])) return } // Batch write to update both sender/receiver request lists let batch = self.db.batch() let requestData = ["timestamp": Timestamp(), "status": "pending"] batch.setData(requestData, forDocument: sentRequestRef) let receivedRequestRef = self.db.collection("friendRequests").document(receiverId).collection("receivedRequests").document(senderId) batch.setData(requestData, forDocument: receivedRequestRef) batch.commit { error in completion(error) } } } }
Accept a Friend Request
This removes the pending request and adds the bidirectional friendship:
func acceptFriendRequest(from senderId: String, for receiverId: String, completion: @escaping (Error?) -> Void) { let batch = db.batch() // Delete the request from both sides let receivedRequestRef = db.collection("friendRequests").document(receiverId).collection("receivedRequests").document(senderId) batch.deleteDocument(receivedRequestRef) let sentRequestRef = db.collection("friendRequests").document(senderId).collection("sentRequests").document(receiverId) batch.deleteDocument(sentRequestRef) // Add bidirectional friendship let receiverFriendRef = db.collection("friends").document(receiverId).collection("userFriends").document(senderId) batch.setData(["addedAt": Timestamp()], forDocument: receiverFriendRef) let senderFriendRef = db.collection("friends").document(senderId).collection("userFriends").document(receiverId) batch.setData(["addedAt": Timestamp()], forDocument: senderFriendRef) batch.commit { error in completion(error) } }
Decline a Friend Request
Simply remove the pending request from both users' lists:
func declineFriendRequest(from senderId: String, for receiverId: String, completion: @escaping (Error?) -> Void) { let batch = db.batch() let receivedRequestRef = db.collection("friendRequests").document(receiverId).collection("receivedRequests").document(senderId) batch.deleteDocument(receivedRequestRef) let sentRequestRef = db.collection("friendRequests").document(senderId).collection("sentRequests").document(receiverId) batch.deleteDocument(sentRequestRef) batch.commit { error in completion(error) } }
Fetch a User's Friends
Get a list of friend IDs, or extend it to fetch full user profiles:
// Fetch friend IDs only func fetchFriendIds(for userId: String, completion: @escaping ([String]?, Error?) -> Void) { db.collection("friends").document(userId).collection("userFriends").getDocuments { snapshot, error in if let error = error { completion(nil, error) return } let friendIds = snapshot?.documents.map { $0.documentID } ?? [] completion(friendIds, nil) } } // Fetch full friend profiles (assuming you have a User Codable model) func fetchFriendProfiles(for userId: String, completion: @escaping ([User]?, Error?) -> Void) { fetchFriendIds(for: userId) { friendIds, error in if let error = error { completion(nil, error) return } guard let friendIds = friendIds, !friendIds.isEmpty else { completion([], nil) return } let dispatchGroup = DispatchGroup() var friends = [User]() for id in friendIds { dispatchGroup.enter() self.db.collection("users").document(id).getDocument { snapshot, error in defer { dispatchGroup.leave() } if let error = error { print("Failed to fetch user \(id): \(error.localizedDescription)") return } if let user = try? snapshot?.data(as: User.self) { friends.append(user) } } } dispatchGroup.notify(queue: .main) { completion(friends, nil) } } }
Real-Time Friend Request Notifications
Add a listener to get real-time updates when new requests come in:
// Define a Codable model for friend requests struct FriendRequest: Codable { var timestamp: Timestamp var status: String var senderId: String? } // Listen for incoming requests func listenForIncomingRequests(for userId: String, completion: @escaping ([FriendRequest]?, Error?) -> Void) -> ListenerRegistration { let requestRef = db.collection("friendRequests").document(userId).collection("receivedRequests") return requestRef.addSnapshotListener { snapshot, error in if let error = error { completion(nil, error) return } let requests = snapshot?.documents.compactMap { doc in var request = try? doc.data(as: FriendRequest.self) request?.senderId = doc.documentID return request } ?? [] completion(requests, nil) } }
Don't forget to remove the listener when it's no longer needed (e.g., in viewWillDisappear).
3. Firebase Security Rules
Lock down your data with these Firestore rules to ensure users can only access their own requests and friends:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Restrict friend requests to the sender/receiver match /friendRequests/{userId}/{requestType}/{otherUserId} { allow read, write: if request.auth.uid == userId; } // Only let users access their own friend list match /friends/{userId}/userFriends/{friendId} { allow read, write: if request.auth.uid == userId; } // Allow public read access to user profiles (adjust if needed) match /users/{userId} { allow read: if true; allow write: if request.auth.uid == userId; } } }
Final Notes
- Adjust the code to match your existing
DataServicestructure (e.g., if you use Realtime Database instead of Firestore, the reference syntax will change, but the core logic of bidirectional relationships remains the same). - Add error handling in your UI to give users clear feedback (e.g., "Request sent!", "Already friends").
- Consider adding a "remove friend" function by deleting the bidirectional entries in the
friendscollection.
内容的提问来源于stack exchange,提问作者Jude Molloy

