feat(ViewModels, Models): 增加 Tweet model,增加上传和获取 tweet 逻辑
增加 Tweet model,增加上传和获取 tweet 逻辑 Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
parent
577643d9b1
commit
9d237961b6
@ -15,6 +15,10 @@
|
||||
2492CC282A0025DD0086C525 /* TweetRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2492CC272A0025DD0086C525 /* TweetRowView.swift */; };
|
||||
24A07CB02A017D3300F4ECA8 /* ExploreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CAF2A017D3300F4ECA8 /* ExploreViewModel.swift */; };
|
||||
24A07CB22A01869F00F4ECA8 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB12A01869F00F4ECA8 /* SearchBar.swift */; };
|
||||
24A07CB42A020ECB00F4ECA8 /* TweetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB32A020ECB00F4ECA8 /* TweetService.swift */; };
|
||||
24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */; };
|
||||
24A07CB82A02173400F4ECA8 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */; };
|
||||
24A07CBA2A02186500F4ECA8 /* Tweet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A07CB92A02186500F4ECA8 /* Tweet.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 */; };
|
||||
@ -58,6 +62,10 @@
|
||||
2492CC272A0025DD0086C525 /* TweetRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetRowView.swift; sourceTree = "<group>"; };
|
||||
24A07CAF2A017D3300F4ECA8 /* ExploreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewModel.swift; sourceTree = "<group>"; };
|
||||
24A07CB12A01869F00F4ECA8 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
|
||||
24A07CB32A020ECB00F4ECA8 /* TweetService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetService.swift; sourceTree = "<group>"; };
|
||||
24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTweetViewModel.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -165,6 +173,7 @@
|
||||
children = (
|
||||
24FA4D9D2A0134CB002D202A /* ImageUploader.swift */,
|
||||
24FA4D9F2A013B24002D202A /* UserService.swift */,
|
||||
24A07CB32A020ECB00F4ECA8 /* TweetService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
@ -173,6 +182,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24FA4DA12A013D2E002D202A /* User.swift */,
|
||||
24A07CB92A02186500F4ECA8 /* Tweet.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@ -210,6 +220,7 @@
|
||||
2492CC222A0022F80086C525 /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24A07CB72A02173400F4ECA8 /* FeedViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
@ -359,6 +370,7 @@
|
||||
24A59AD92A00DB67009C9E3E /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24A07CB52A020FF900F4ECA8 /* UploadTweetViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
@ -493,6 +505,8 @@
|
||||
24FA4DA02A013B24002D202A /* UserService.swift in Sources */,
|
||||
24FA4D9C2A012E1A002D202A /* ImagePicker.swift in Sources */,
|
||||
2492CC0F2A000EB00086C525 /* ContentView.swift in Sources */,
|
||||
24A07CBA2A02186500F4ECA8 /* Tweet.swift in Sources */,
|
||||
24A07CB82A02173400F4ECA8 /* FeedViewModel.swift in Sources */,
|
||||
2492CC282A0025DD0086C525 /* TweetRowView.swift in Sources */,
|
||||
24FA4D9E2A0134CB002D202A /* ImageUploader.swift in Sources */,
|
||||
24A59AEA2A00F672009C9E3E /* CustomInputField.swift in Sources */,
|
||||
@ -503,6 +517,8 @@
|
||||
24A07CB02A017D3300F4ECA8 /* ExploreViewModel.swift in Sources */,
|
||||
2492CC0D2A000EB00086C525 /* dudu_tweetApp.swift in Sources */,
|
||||
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */,
|
||||
24A07CB62A020FF900F4ECA8 /* UploadTweetViewModel.swift in Sources */,
|
||||
24A07CB42A020ECB00F4ECA8 /* TweetService.swift in Sources */,
|
||||
24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */,
|
||||
24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */,
|
||||
24A07CB22A01869F00F4ECA8 /* SearchBar.swift in Sources */,
|
||||
|
||||
Binary file not shown.
@ -6,35 +6,45 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct TweetRowView: View {
|
||||
let tweet: Tweet
|
||||
|
||||
init(tweet: Tweet) {
|
||||
self.tweet = tweet
|
||||
}
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
// profile image + user info + tweet
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
// profile image
|
||||
Circle()
|
||||
.frame(width: 56, height: 56)
|
||||
.foregroundColor(Color(.systemBlue))
|
||||
|
||||
// user info + tweet caption
|
||||
VStack(alignment: .leading) {
|
||||
// user info
|
||||
HStack {
|
||||
Text("Zhang San")
|
||||
.font(.subheadline).bold()
|
||||
Text("@zhang3")
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
Text("2min")
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
}
|
||||
if let user = tweet.user {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
// profile image
|
||||
KFImage(URL(string: user.profileImageUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.clipShape(Circle())
|
||||
.frame(width: 56, height: 56)
|
||||
|
||||
// tweet caption
|
||||
Text("全都不是我干的。")
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.leading)
|
||||
// user info + tweet caption
|
||||
VStack(alignment: .leading) {
|
||||
// user info
|
||||
HStack {
|
||||
Text(user.fullname)
|
||||
.font(.subheadline).bold()
|
||||
Text("@\(user.username)")
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
Text("2min")
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
// tweet caption
|
||||
Text(tweet.caption)
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
// action buttons
|
||||
@ -75,9 +85,9 @@ struct TweetRowView: View {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TweetRowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TweetRowView()
|
||||
}
|
||||
}
|
||||
//
|
||||
//struct TweetRowView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// TweetRowView()
|
||||
// }
|
||||
//}
|
||||
|
||||
@ -34,7 +34,6 @@ class ExploreViewModel: ObservableObject {
|
||||
func fetchUsers() {
|
||||
service.fetchUsers { users in
|
||||
self.users = users
|
||||
print("DEBUG: users \(users)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,13 +9,14 @@ import SwiftUI
|
||||
|
||||
struct FeedView: View {
|
||||
@State private var showNewTweetView = false
|
||||
@ObservedObject var viewModel = FeedViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(0 ... 20, id: \.self) { _ in
|
||||
TweetRowView()
|
||||
ForEach(viewModel.tweets) { tweet in
|
||||
TweetRowView(tweet: tweet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
//
|
||||
// FeedViewModel.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FeedViewModel: ObservableObject {
|
||||
@Published var tweets = [Tweet]()
|
||||
let service = TweetService()
|
||||
let userService = UserService()
|
||||
|
||||
init() {
|
||||
fetchTweets()
|
||||
}
|
||||
|
||||
func fetchTweets() {
|
||||
service.fetchTweets { tweets in
|
||||
self.tweets = tweets
|
||||
for i in 0 ..< tweets.count {
|
||||
let uid = tweets[i].uid
|
||||
self.userService.fetchUser(withUid: uid) { user in
|
||||
self.tweets[i].user = user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,7 +166,7 @@ extension ProfileView {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(0...9, id: \.self) { _ in
|
||||
TweetRowView()
|
||||
// TweetRowView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
//
|
||||
// UploadTweetViewModel.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UploadTweetViewModel: ObservableObject {
|
||||
@Published var didUploadTweet = false
|
||||
let service = TweetService()
|
||||
|
||||
func uploadTweet(withCaption caption: String) {
|
||||
service.uploadTweet(caption: caption) { success in
|
||||
if success {
|
||||
self.didUploadTweet = true
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,51 +6,65 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct NewTweetView: View {
|
||||
@State private var caption = ""
|
||||
// @Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject var authViewModel: AuthViewModel
|
||||
@ObservedObject var viewModel = UploadTweetViewModel()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Button {
|
||||
// presentationMode.wrappedValue.dismiss()
|
||||
if let user = authViewModel.currentUser {
|
||||
VStack {
|
||||
HStack {
|
||||
Button {
|
||||
// presentationMode.wrappedValue.dismiss()
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.foregroundColor(Color(.systemBlue))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
viewModel.uploadTweet(withCaption: caption)
|
||||
} label: {
|
||||
Text("Tweet")
|
||||
.bold()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(.systemBlue))
|
||||
.foregroundColor(.white)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
HStack(alignment: .top) {
|
||||
KFImage(URL(string: user.profileImageUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.clipShape(Circle())
|
||||
.frame(width: 64, height: 64)
|
||||
TextArea("What's happening?", text: $caption)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onReceive(viewModel.$didUploadTweet) { success in
|
||||
if success {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.foregroundColor(Color(.systemBlue))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
print("tweet this..")
|
||||
} label: {
|
||||
Text("Tweet")
|
||||
.bold()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(.systemBlue))
|
||||
.foregroundColor(.white)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
HStack(alignment: .top) {
|
||||
Circle()
|
||||
.frame(width: 64, height: 64)
|
||||
TextArea("What's happening?", text: $caption)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct NewTweetView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NewTweetView()
|
||||
}
|
||||
}
|
||||
//
|
||||
//struct NewTweetView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// NewTweetView()
|
||||
// }
|
||||
//}
|
||||
|
||||
19
dudu-tweet/dudu-tweet/Model/Tweet.swift
Normal file
19
dudu-tweet/dudu-tweet/Model/Tweet.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Tweet.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/3.
|
||||
//
|
||||
|
||||
import FirebaseFirestoreSwift
|
||||
import Firebase
|
||||
|
||||
struct Tweet: Identifiable, Decodable {
|
||||
@DocumentID var id: String?
|
||||
let caption: String
|
||||
let timestamp: Timestamp
|
||||
let uid: String
|
||||
var likes: Int
|
||||
|
||||
var user: User?
|
||||
}
|
||||
41
dudu-tweet/dudu-tweet/Service/TweetService.swift
Normal file
41
dudu-tweet/dudu-tweet/Service/TweetService.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// TweetService.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/3.
|
||||
//
|
||||
|
||||
import Firebase
|
||||
|
||||
struct TweetService {
|
||||
|
||||
func uploadTweet(caption: String, completion: @escaping(Bool) -> Void) {
|
||||
guard let uid = Auth.auth().currentUser?.uid else { return }
|
||||
|
||||
let data = ["uid": uid,
|
||||
"caption": caption,
|
||||
"likes": 0,
|
||||
"timestamp": Timestamp(date: Date())] as [String : Any]
|
||||
|
||||
Firestore.firestore().collection("tweets").document()
|
||||
.setData(data) { error in
|
||||
if let error = error {
|
||||
print("DEBUG: uploaded tweet with error \(error)")
|
||||
completion(false)
|
||||
}
|
||||
completion(true)
|
||||
print("DEBUG: uploaded tweet...")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchTweets(completion: @escaping([Tweet]) -> Void) {
|
||||
Firestore.firestore().collection("tweets").getDocuments { snapshot, _ in
|
||||
guard let documents = snapshot?.documents else { return }
|
||||
|
||||
let tweets = documents.compactMap({ try? $0.data(as: Tweet.self) })
|
||||
completion(tweets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,6 @@ struct UserService {
|
||||
.getDocument { snapshot, _ in
|
||||
guard let snapshot = snapshot else { return }
|
||||
guard let user = try? snapshot.data(as: User.self) else { return }
|
||||
print("DEBUG: Username is \(user.username)")
|
||||
print("DEBUG: User id is \(user.id)")
|
||||
completion(user)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user