feat(ViewModels, Views): 获取 liked tweets,在 Profile 页面展示

获取 liked tweets,在 Profile 页面展示

Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
Ching 2023-05-03 17:26:26 +08:00
parent 5b7648e6e7
commit d34651e306
9 changed files with 164 additions and 9 deletions

View File

@ -20,6 +20,7 @@
24A07CB82A02173400F4ECA8 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */; };
24A07CBA2A02186500F4ECA8 /* Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB92A02186500F4ECA8 /* Tweet.swift */; };
24A07CBC2A022B2000F4ECA8 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CBB2A022B2000F4ECA8 /* ProfileViewModel.swift */; };
24A07CC22A02302700F4ECA8 /* TweetRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */; };
24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB32A002EB8009C9E3E /* MainTabView.swift */; };
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AB92A0030CB009C9E3E /* ExploreView.swift */; };
24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */; };
@ -68,6 +69,7 @@
24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; };
24A07CB92A02186500F4ECA8 /* Tweet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tweet.swift; sourceTree = "<group>"; };
24A07CBB2A022B2000F4ECA8 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = "<group>"; };
24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetRowViewModel.swift; sourceTree = "<group>"; };
24A59AB32A002EB8009C9E3E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
24A59AB92A0030CB009C9E3E /* ExploreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreView.swift; sourceTree = "<group>"; };
24A59ABB2A0030EC009C9E3E /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
@ -238,11 +240,28 @@
2492CC262A0025A50086C525 /* Tweets */ = {
isa = PBXGroup;
children = (
2492CC272A0025DD0086C525 /* TweetRowView.swift */,
24A07CBE2A022FCB00F4ECA8 /* ViewModels */,
24A07CBD2A022FC600F4ECA8 /* Views */,
);
path = Tweets;
sourceTree = "<group>";
};
24A07CBD2A022FC600F4ECA8 /* Views */ = {
isa = PBXGroup;
children = (
2492CC272A0025DD0086C525 /* TweetRowView.swift */,
);
path = Views;
sourceTree = "<group>";
};
24A07CBE2A022FCB00F4ECA8 /* ViewModels */ = {
isa = PBXGroup;
children = (
24A07CC12A02302700F4ECA8 /* TweetRowViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
24A59AB22A002E79009C9E3E /* MainTab */ = {
isa = PBXGroup;
children = (
@ -522,6 +541,7 @@
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */,
24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */,
24A07CB42A020ECB00F4ECA8 /* TweetService.swift in Sources */,
24A07CC22A02302700F4ECA8 /* TweetRowViewModel.swift in Sources */,
24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */,
24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */,
24A07CB22A01869F00F4ECA8 /* SearchBar.swift in Sources */,

View File

@ -0,0 +1,38 @@
//
// TweetRowViewModel.swift
// dudu-tweet
//
// Created by ching on 2023/5/3.
//
import Foundation
class TweetVeiwModel: ObservableObject {
private let service = TweetService()
@Published var tweet: Tweet
init(tweet: Tweet) {
self.tweet = tweet
self.checkUserLikedTweet()
}
func likeTweet() {
service.likeTweet(tweet) { _ in
self.tweet.didLike = true
}
}
func unlikeTweet() {
service.unlikeTweet(tweet) { _ in
self.tweet.didLike = false
}
}
func checkUserLikedTweet() {
service.checkIfUserLikedTweet(tweet) { didLiked in
if didLiked {
self.tweet.didLike = true
}
}
}
}

View File

@ -9,15 +9,15 @@ import SwiftUI
import Kingfisher
struct TweetRowView: View {
let tweet: Tweet
@ObservedObject var viewModel: TweetVeiwModel
init(tweet: Tweet) {
self.tweet = tweet
self.viewModel = TweetVeiwModel(tweet: tweet)
}
var body: some View {
VStack(alignment: .leading) {
// profile image + user info + tweet
if let user = tweet.user {
if let user = viewModel.tweet.user {
HStack(alignment: .top, spacing: 12) {
// profile image
KFImage(URL(string: user.profileImageUrl))
@ -41,7 +41,7 @@ struct TweetRowView: View {
}
// tweet caption
Text(tweet.caption)
Text(viewModel.tweet.caption)
.font(.subheadline)
.multilineTextAlignment(.leading)
}
@ -64,10 +64,11 @@ struct TweetRowView: View {
}
Spacer()
Button {
// action here
viewModel.tweet.didLike ?? false ? viewModel.unlikeTweet() : viewModel.likeTweet()
} label: {
Image(systemName: "heart")
Image(systemName: viewModel.tweet.didLike ?? false ? "heart.fill" : "heart")
.font(.subheadline)
.foregroundColor(viewModel.tweet.didLike ?? false ? .red : .gray)
}
Spacer()
Button {

View File

@ -10,12 +10,30 @@ import Foundation
class ProfileViewModel: ObservableObject {
@Published var tweets = [Tweet]()
@Published var likedTweets = [Tweet]()
private let service = TweetService()
private let userService = UserService()
let user: User
init(user: User) {
self.user = user
self.fetchUserTweet()
self.fetchLikedTweets()
}
var actionBarTitle: String {
user.isCurrentUser ? "Edit Profile" : "Follow"
}
func tweets(forFilter filter: TweetFilterViewModel) -> [Tweet] {
switch filter {
case .tweets:
return tweets
case .replies:
return tweets
case .likes:
return likedTweets
}
}
func fetchUserTweet() {
@ -27,4 +45,19 @@ class ProfileViewModel: ObservableObject {
}
}
}
func fetchLikedTweets() {
guard let uid = user.id else { return }
service.fetchLikedTweets(forUid: uid) { tweets in
self.likedTweets = tweets
for i in 0 ..< tweets.count {
let uid = tweets[i].uid
self.userService.fetchUser(withUid: uid) { user in
self.likedTweets[i].user = user
}
}
}
}
}

View File

@ -87,7 +87,7 @@ extension ProfileView {
Button {
// action here
} label: {
Text("Edit Profile")
Text(viewModel.actionBarTitle)
.font(.subheadline).bold()
.frame(width: 120, height: 32)
.foregroundColor(.black)
@ -165,7 +165,7 @@ extension ProfileView {
var tweetsView: some View {
ScrollView {
LazyVStack {
ForEach(viewModel.tweets) { tweet in
ForEach(viewModel.tweets(forFilter: self.selectedFilter)) { tweet in
TweetRowView(tweet: tweet)
}
}

View File

@ -16,4 +16,5 @@ struct Tweet: Identifiable, Decodable {
var likes: Int
var user: User?
var didLike: Bool? = false
}

View File

@ -6,6 +6,7 @@
//
import FirebaseFirestoreSwift
import Firebase
struct User: Identifiable, Decodable {
@DocumentID var id: String?
@ -13,4 +14,6 @@ struct User: Identifiable, Decodable {
let fullname: String
let profileImageUrl: String
let email: String
var isCurrentUser: Bool { return Auth.auth().currentUser?.uid == self.id}
}

View File

@ -50,5 +50,64 @@ struct TweetService {
completion(tweets.sorted(by: { $0.timestamp.dateValue() > $1.timestamp.dateValue()}))
}
}
func likeTweet(_ tweet: Tweet, completion: @escaping(Bool) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let tweetId = tweet.id else { return }
let userLikeRef = Firestore.firestore().collection("users").document(uid).collection("user-likes")
Firestore.firestore().collection("tweets").document(tweetId)
.updateData(["likes": tweet.likes + 1]) { _ in
userLikeRef.document(tweetId).setData([:]) { _ in
completion(true)
}
}
}
func unlikeTweet(_ tweet: Tweet, completion: @escaping(Bool) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let tweetId = tweet.id else { return }
guard tweet.likes > 0 else { return }
let userLikeRef = Firestore.firestore().collection("users").document(uid).collection("user-likes")
Firestore.firestore().collection("tweets").document(tweetId)
.updateData(["likes": tweet.likes - 1]) { _ in
userLikeRef.document(tweetId).delete() { _ in
completion(true)
}
}
}
func checkIfUserLikedTweet(_ tweet: Tweet, completion: @escaping(Bool) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let tweetId = tweet.id else { return }
print("DEBUG: checking likes for \(tweetId)")
Firestore.firestore().collection("users").document(uid).collection("user-likes")
.document(tweetId)
.getDocument { snapshot, _ in
guard let snapshot = snapshot else { return }
completion(snapshot.exists)
}
}
func fetchLikedTweets(forUid uid: String, completion: @escaping([Tweet]) -> Void) {
var tweets = [Tweet]()
Firestore.firestore().collection("users").document(uid).collection("user-likes")
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
documents.forEach { doc in
let tweetID = doc.documentID
Firestore.firestore().collection("tweets")
.document(tweetID)
.getDocument { snapshot, _ in
guard let tweet = try? snapshot?.data(as: Tweet.self) else { return }
tweets.append(tweet)
completion(tweets)
}
}
}
}
}