Compare commits

...

6 Commits

Author SHA1 Message Date
Ching
62c6c6d2c5 feat(Model): 增加 User model,用获取到的 User 数据替换写死的数据
增加 User model,用获取到的 User 数据替换写死的数据

Signed-off-by: Ching <loooching@gmail.com>
2023-05-02 22:01:03 +08:00
Ching
3eb9b13f86 feat(Views): 增加 profileImageUpload page,增加图片上传功能
增加 profileImageUpload page,增加图片上传功能

Signed-off-by: Ching <loooching@gmail.com>
2023-05-02 20:34:49 +08:00
Ching
698f18624c feat(dependency): 增加 Firebase 依赖
增加 Firebase 依赖

Signed-off-by: Ching <loooching@gmail.com>
2023-05-02 16:53:53 +08:00
Ching
25dfd09568 feat(Views): 增加 login page 和 registration page
增加 login page 和 registration page

Signed-off-by: Ching <loooching@gmail.com>
2023-05-02 16:17:17 +08:00
Ching
3356aec1cd feat(Views): 增加 NewTweet 页面
增加 NewTweet 页面

Signed-off-by: Ching <loooching@gmail.com>
2023-05-02 15:10:13 +08:00
Ching
6afccd6445 feat(Views): 增加 SideMenu page
增加 SideMenu page

Signed-off-by: Ching <loooching@gmail.com>
2023-05-02 13:30:54 +08:00
29 changed files with 1463 additions and 58 deletions

34
GoogleService-Info.plist Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>77785854711-kedg65qete3dhcmtbda2nn0n4e2sa9h9.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.77785854711-kedg65qete3dhcmtbda2nn0n4e2sa9h9</string>
<key>API_KEY</key>
<string>AIzaSyBXhDkQ_dGzAchKiBCsEJL78fAKd77EBHg</string>
<key>GCM_SENDER_ID</key>
<string>77785854711</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.tunpok.ios.dudu-tweet</string>
<key>PROJECT_ID</key>
<string>twitter-22a99</string>
<key>STORAGE_BUCKET</key>
<string>twitter-22a99.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:77785854711:ios:00f7d7cd05ce7e37a719f2</string>
</dict>
</plist>

View File

@ -19,6 +19,30 @@
24A59ABE2A003108009C9E3E /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ABD2A003108009C9E3E /* MessagesView.swift */; };
24A59AC22A003249009C9E3E /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AC12A003249009C9E3E /* ProfileView.swift */; };
24A59AC42A003A52009C9E3E /* TwewtFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AC32A003A52009C9E3E /* TwewtFilterViewModel.swift */; };
24A59AC92A00BA81009C9E3E /* UserRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AC82A00BA81009C9E3E /* UserRowView.swift */; };
24A59ACE2A00BDCB009C9E3E /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ACD2A00BDCB009C9E3E /* SideMenuView.swift */; };
24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AD12A00BE14009C9E3E /* SideMenuViewModel.swift */; };
24A59AD42A00C07D009C9E3E /* UserStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AD32A00C07D009C9E3E /* UserStatsView.swift */; };
24A59AD62A00CA82009C9E3E /* SideMenuOptionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AD52A00CA82009C9E3E /* SideMenuOptionRowView.swift */; };
24A59ADD2A00DB9F009C9E3E /* NewTweetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ADC2A00DB9F009C9E3E /* NewTweetView.swift */; };
24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59ADE2A00DCC2009C9E3E /* TextArea.swift */; };
24A59AE42A00EF1F009C9E3E /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AE32A00EF1F009C9E3E /* LoginView.swift */; };
24A59AE62A00EF3A009C9E3E /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AE52A00EF3A009C9E3E /* RegistrationView.swift */; };
24A59AE82A00F106009C9E3E /* RoundedShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AE72A00F106009C9E3E /* RoundedShape.swift */; };
24A59AEA2A00F672009C9E3E /* CustomInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AE92A00F672009C9E3E /* CustomInputField.swift */; };
24A59AED2A00F942009C9E3E /* AuthHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AEC2A00F942009C9E3E /* AuthHeaderView.swift */; };
24A59AEF2A010142009C9E3E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 24A59AEE2A010142009C9E3E /* GoogleService-Info.plist */; };
24A59AF22A010411009C9E3E /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF12A010411009C9E3E /* FirebaseAuth */; };
24A59AF42A010411009C9E3E /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF32A010411009C9E3E /* FirebaseFirestore */; };
24A59AF62A010411009C9E3E /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF52A010411009C9E3E /* FirebaseFirestoreSwift */; };
24A59AF82A010411009C9E3E /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF72A010411009C9E3E /* FirebaseStorage */; };
24A59AFA2A01081F009C9E3E /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AF92A01081F009C9E3E /* AuthViewModel.swift */; };
24FA4D992A012A5D002D202A /* ProfilePhotoSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */; };
24FA4D9C2A012E1A002D202A /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D9B2A012E1A002D202A /* ImagePicker.swift */; };
24FA4D9E2A0134CB002D202A /* ImageUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D9D2A0134CB002D202A /* ImageUploader.swift */; };
24FA4DA02A013B24002D202A /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D9F2A013B24002D202A /* UserService.swift */; };
24FA4DA22A013D2E002D202A /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4DA12A013D2E002D202A /* User.swift */; };
24FA4DA52A0142E2002D202A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 24FA4DA42A0142E2002D202A /* Kingfisher */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -36,6 +60,25 @@
24A59ABD2A003108009C9E3E /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = "<group>"; };
24A59AC12A003249009C9E3E /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
24A59AC32A003A52009C9E3E /* TwewtFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwewtFilterViewModel.swift; sourceTree = "<group>"; };
24A59AC82A00BA81009C9E3E /* UserRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRowView.swift; sourceTree = "<group>"; };
24A59ACD2A00BDCB009C9E3E /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
24A59AD12A00BE14009C9E3E /* SideMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuViewModel.swift; sourceTree = "<group>"; };
24A59AD32A00C07D009C9E3E /* UserStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatsView.swift; sourceTree = "<group>"; };
24A59AD52A00CA82009C9E3E /* SideMenuOptionRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuOptionRowView.swift; sourceTree = "<group>"; };
24A59ADC2A00DB9F009C9E3E /* NewTweetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTweetView.swift; sourceTree = "<group>"; };
24A59ADE2A00DCC2009C9E3E /* TextArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextArea.swift; sourceTree = "<group>"; };
24A59AE32A00EF1F009C9E3E /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
24A59AE52A00EF3A009C9E3E /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
24A59AE72A00F106009C9E3E /* RoundedShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedShape.swift; sourceTree = "<group>"; };
24A59AE92A00F672009C9E3E /* CustomInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInputField.swift; sourceTree = "<group>"; };
24A59AEC2A00F942009C9E3E /* AuthHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHeaderView.swift; sourceTree = "<group>"; };
24A59AEE2A010142009C9E3E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../GoogleService-Info.plist"; sourceTree = "<group>"; };
24A59AF92A01081F009C9E3E /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = "<group>"; };
24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePhotoSelectorView.swift; sourceTree = "<group>"; };
24FA4D9B2A012E1A002D202A /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
24FA4D9D2A0134CB002D202A /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = "<group>"; };
24FA4D9F2A013B24002D202A /* UserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = "<group>"; };
24FA4DA12A013D2E002D202A /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -43,6 +86,11 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
24A59AF22A010411009C9E3E /* FirebaseAuth in Frameworks */,
24A59AF82A010411009C9E3E /* FirebaseStorage in Frameworks */,
24A59AF42A010411009C9E3E /* FirebaseFirestore in Frameworks */,
24A59AF62A010411009C9E3E /* FirebaseFirestoreSwift in Frameworks */,
24FA4DA52A0142E2002D202A /* Kingfisher in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -68,6 +116,7 @@
2492CC0B2A000EB00086C525 /* dudu-tweet */ = {
isa = PBXGroup;
children = (
24FA4D9A2A012E05002D202A /* Utils */,
2492CC1F2A0022990086C525 /* Extensions */,
2492CC1E2A0022970086C525 /* Model */,
2492CC1D2A0022960086C525 /* Service */,
@ -75,6 +124,7 @@
2492CC0C2A000EB00086C525 /* dudu_tweetApp.swift */,
2492CC0E2A000EB00086C525 /* ContentView.swift */,
2492CC102A000EB10086C525 /* Assets.xcassets */,
24A59AEE2A010142009C9E3E /* GoogleService-Info.plist */,
2492CC122A000EB10086C525 /* dudu_tweet.entitlements */,
2492CC132A000EB10086C525 /* Preview Content */,
);
@ -92,6 +142,9 @@
2492CC1B2A00228F0086C525 /* Core */ = {
isa = PBXGroup;
children = (
24A59AE02A00EEE8009C9E3E /* Authentication */,
24A59AD72A00DB49009C9E3E /* UploadTweet */,
24A59ACA2A00BDA1009C9E3E /* SideMenu */,
2492CC212A0022C30086C525 /* Components */,
24A59AB82A00308E009C9E3E /* Profile */,
24A59AB72A00305E009C9E3E /* Explore */,
@ -106,6 +159,8 @@
2492CC1D2A0022960086C525 /* Service */ = {
isa = PBXGroup;
children = (
24FA4D9D2A0134CB002D202A /* ImageUploader.swift */,
24FA4D9F2A013B24002D202A /* UserService.swift */,
);
path = Service;
sourceTree = "<group>";
@ -113,6 +168,7 @@
2492CC1E2A0022970086C525 /* Model */ = {
isa = PBXGroup;
children = (
24FA4DA12A013D2E002D202A /* User.swift */,
);
path = Model;
sourceTree = "<group>";
@ -136,7 +192,12 @@
2492CC212A0022C30086C525 /* Components */ = {
isa = PBXGroup;
children = (
24A59AEB2A00F92D009C9E3E /* Authentication */,
24A59AC72A00BA6A009C9E3E /* Users */,
2492CC262A0025A50086C525 /* Tweets */,
24A59ADE2A00DCC2009C9E3E /* TextArea.swift */,
24A59AE72A00F106009C9E3E /* RoundedShape.swift */,
24A59AE92A00F672009C9E3E /* CustomInputField.swift */,
);
path = Components;
sourceTree = "<group>";
@ -191,7 +252,8 @@
24A59AB72A00305E009C9E3E /* Explore */ = {
isa = PBXGroup;
children = (
24A59AB92A0030CB009C9E3E /* ExploreView.swift */,
24A59AC62A00BA47009C9E3E /* Views */,
24A59AC52A00BA3A009C9E3E /* ViewModels */,
);
path = Explore;
sourceTree = "<group>";
@ -221,6 +283,123 @@
path = ViewModels;
sourceTree = "<group>";
};
24A59AC52A00BA3A009C9E3E /* ViewModels */ = {
isa = PBXGroup;
children = (
);
path = ViewModels;
sourceTree = "<group>";
};
24A59AC62A00BA47009C9E3E /* Views */ = {
isa = PBXGroup;
children = (
24A59AB92A0030CB009C9E3E /* ExploreView.swift */,
);
path = Views;
sourceTree = "<group>";
};
24A59AC72A00BA6A009C9E3E /* Users */ = {
isa = PBXGroup;
children = (
24A59AC82A00BA81009C9E3E /* UserRowView.swift */,
24A59AD32A00C07D009C9E3E /* UserStatsView.swift */,
);
path = Users;
sourceTree = "<group>";
};
24A59ACA2A00BDA1009C9E3E /* SideMenu */ = {
isa = PBXGroup;
children = (
24A59ACC2A00BDB7009C9E3E /* ViewModels */,
24A59ACB2A00BDAF009C9E3E /* Views */,
);
path = SideMenu;
sourceTree = "<group>";
};
24A59ACB2A00BDAF009C9E3E /* Views */ = {
isa = PBXGroup;
children = (
24A59ACD2A00BDCB009C9E3E /* SideMenuView.swift */,
24A59AD52A00CA82009C9E3E /* SideMenuOptionRowView.swift */,
);
path = Views;
sourceTree = "<group>";
};
24A59ACC2A00BDB7009C9E3E /* ViewModels */ = {
isa = PBXGroup;
children = (
24A59AD12A00BE14009C9E3E /* SideMenuViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
24A59AD72A00DB49009C9E3E /* UploadTweet */ = {
isa = PBXGroup;
children = (
24A59AD92A00DB67009C9E3E /* ViewModels */,
24A59AD82A00DB5A009C9E3E /* Views */,
);
path = UploadTweet;
sourceTree = "<group>";
};
24A59AD82A00DB5A009C9E3E /* Views */ = {
isa = PBXGroup;
children = (
24A59ADC2A00DB9F009C9E3E /* NewTweetView.swift */,
);
path = Views;
sourceTree = "<group>";
};
24A59AD92A00DB67009C9E3E /* ViewModels */ = {
isa = PBXGroup;
children = (
);
path = ViewModels;
sourceTree = "<group>";
};
24A59AE02A00EEE8009C9E3E /* Authentication */ = {
isa = PBXGroup;
children = (
24A59AE22A00EEF8009C9E3E /* ViewModels */,
24A59AE12A00EEF0009C9E3E /* Views */,
);
path = Authentication;
sourceTree = "<group>";
};
24A59AE12A00EEF0009C9E3E /* Views */ = {
isa = PBXGroup;
children = (
24A59AE32A00EF1F009C9E3E /* LoginView.swift */,
24A59AE52A00EF3A009C9E3E /* RegistrationView.swift */,
24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */,
);
path = Views;
sourceTree = "<group>";
};
24A59AE22A00EEF8009C9E3E /* ViewModels */ = {
isa = PBXGroup;
children = (
24A59AF92A01081F009C9E3E /* AuthViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
24A59AEB2A00F92D009C9E3E /* Authentication */ = {
isa = PBXGroup;
children = (
24A59AEC2A00F942009C9E3E /* AuthHeaderView.swift */,
);
path = Authentication;
sourceTree = "<group>";
};
24FA4D9A2A012E05002D202A /* Utils */ = {
isa = PBXGroup;
children = (
24FA4D9B2A012E1A002D202A /* ImagePicker.swift */,
);
path = Utils;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -237,6 +416,13 @@
dependencies = (
);
name = "dudu-tweet";
packageProductDependencies = (
24A59AF12A010411009C9E3E /* FirebaseAuth */,
24A59AF32A010411009C9E3E /* FirebaseFirestore */,
24A59AF52A010411009C9E3E /* FirebaseFirestoreSwift */,
24A59AF72A010411009C9E3E /* FirebaseStorage */,
24FA4DA42A0142E2002D202A /* Kingfisher */,
);
productName = "dudu-tweet";
productReference = 2492CC092A000EB00086C525 /* dudu-tweet.app */;
productType = "com.apple.product-type.application";
@ -265,6 +451,10 @@
Base,
);
mainGroup = 2492CC002A000EB00086C525;
packageReferences = (
24A59AF02A010411009C9E3E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
24FA4DA32A0142E2002D202A /* XCRemoteSwiftPackageReference "Kingfisher" */,
);
productRefGroup = 2492CC0A2A000EB00086C525 /* Products */;
projectDirPath = "";
projectRoot = "";
@ -281,6 +471,7 @@
files = (
2492CC152A000EB10086C525 /* Preview Assets.xcassets in Resources */,
2492CC112A000EB10086C525 /* Assets.xcassets in Resources */,
24A59AEF2A010142009C9E3E /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -293,14 +484,32 @@
files = (
24A59ABE2A003108009C9E3E /* MessagesView.swift in Sources */,
24A59AC22A003249009C9E3E /* ProfileView.swift in Sources */,
24FA4DA02A013B24002D202A /* UserService.swift in Sources */,
24FA4D9C2A012E1A002D202A /* ImagePicker.swift in Sources */,
2492CC0F2A000EB00086C525 /* ContentView.swift in Sources */,
2492CC282A0025DD0086C525 /* TweetRowView.swift in Sources */,
24FA4D9E2A0134CB002D202A /* ImageUploader.swift in Sources */,
24A59AEA2A00F672009C9E3E /* CustomInputField.swift in Sources */,
24A59AC42A003A52009C9E3E /* TwewtFilterViewModel.swift in Sources */,
2492CC252A0023220086C525 /* FeedView.swift in Sources */,
24A59ACE2A00BDCB009C9E3E /* SideMenuView.swift in Sources */,
24FA4D992A012A5D002D202A /* ProfilePhotoSelectorView.swift in Sources */,
2492CC0D2A000EB00086C525 /* dudu_tweetApp.swift in Sources */,
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */,
24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */,
24A59ADF2A00DCC2009C9E3E /* TextArea.swift in Sources */,
24A59AD62A00CA82009C9E3E /* SideMenuOptionRowView.swift in Sources */,
24A59AE42A00EF1F009C9E3E /* LoginView.swift in Sources */,
24A59AE62A00EF3A009C9E3E /* RegistrationView.swift in Sources */,
24A59ADD2A00DB9F009C9E3E /* NewTweetView.swift in Sources */,
24FA4DA22A013D2E002D202A /* User.swift in Sources */,
24A59AED2A00F942009C9E3E /* AuthHeaderView.swift in Sources */,
24A59AFA2A01081F009C9E3E /* AuthViewModel.swift in Sources */,
24A59AE82A00F106009C9E3E /* RoundedShape.swift in Sources */,
24A59AC92A00BA81009C9E3E /* UserRowView.swift in Sources */,
24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */,
24A59ABC2A0030EC009C9E3E /* NotificationsView.swift in Sources */,
24A59AD42A00C07D009C9E3E /* UserStatsView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -514,6 +723,53 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
24A59AF02A010411009C9E3E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 9.0.0;
};
};
24FA4DA32A0142E2002D202A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 7.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
24A59AF12A010411009C9E3E /* FirebaseAuth */ = {
isa = XCSwiftPackageProductDependency;
package = 24A59AF02A010411009C9E3E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseAuth;
};
24A59AF32A010411009C9E3E /* FirebaseFirestore */ = {
isa = XCSwiftPackageProductDependency;
package = 24A59AF02A010411009C9E3E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseFirestore;
};
24A59AF52A010411009C9E3E /* FirebaseFirestoreSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 24A59AF02A010411009C9E3E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseFirestoreSwift;
};
24A59AF72A010411009C9E3E /* FirebaseStorage */ = {
isa = XCSwiftPackageProductDependency;
package = 24A59AF02A010411009C9E3E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseStorage;
};
24FA4DA42A0142E2002D202A /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 24FA4DA32A0142E2002D202A /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 2492CC012A000EB00086C525 /* Project object */;
}

View File

@ -0,0 +1,122 @@
{
"pins" : [
{
"identity" : "abseil-cpp-swiftpm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git",
"state" : {
"revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1",
"version" : "0.20220203.2"
}
},
{
"identity" : "boringssl-swiftpm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/boringssl-SwiftPM.git",
"state" : {
"revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab",
"version" : "0.9.1"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050",
"version" : "9.6.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "c1cfde8067668027b23a42c29d11c246152fe046",
"version" : "9.6.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "cc7265b8e3906304e6e81f32c1662a94bbae2357",
"version" : "9.2.2"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "871d43135925cde39ef7421d8723ce47edfdcc39",
"version" : "7.11.1"
}
},
{
"identity" : "grpc-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-ios.git",
"state" : {
"revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6",
"version" : "1.44.3-grpc"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "5ccda3981422a84186387dbb763ba739178b529c",
"version" : "2.3.0"
}
},
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher.git",
"state" : {
"revision" : "af4be924ad984cf4d16f4ae4df424e79a443d435",
"version" : "7.6.2"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
"version" : "1.22.2"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
"version" : "2.30909.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a",
"version" : "2.2.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "0af9125c4eae12a4973fb66574c53a54962a9e1e",
"version" : "1.21.0"
}
}
],
"version" : 2
}

View File

@ -4,10 +4,31 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>Promises (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>Promises (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>Promises (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>dudu-tweet.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -6,16 +6,22 @@
//
import SwiftUI
import Kingfisher
struct ContentView: View {
@State private var showMenu = false
@EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
Group {
// no user loggin
if viewModel.userSession == nil {
LoginView()
} else {
// have a logged in user
mainInterfaceView
}
}
.padding()
}
}
@ -24,3 +30,52 @@ struct ContentView_Previews: PreviewProvider {
ContentView()
}
}
extension ContentView {
var mainInterfaceView: some View {
ZStack(alignment: .topLeading) {
MainTabView()
.toolbar(showMenu ? .hidden : .visible)
if showMenu {
ZStack {
Color(.black)
.opacity(showMenu ? 0.25 : 0.0)
}.onTapGesture {
withAnimation(.easeInOut) {
showMenu = false
}
}
.ignoresSafeArea()
}
SideMenuView()
.frame(width: 300)
.offset(x: showMenu ? 0: -300, y: 0)
.background(showMenu ? Color.white: Color.clear)
}
.navigationTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if let user = viewModel.currentUser {
Button {
withAnimation(.easeInOut) {
showMenu.toggle()
}
} label: {
KFImage(URL(string: user.profileImageUrl))
.resizable()
.scaledToFill()
.clipShape(Circle())
.frame(width: 32, height: 32)
}
}
}
}
.onAppear {
showMenu = false
}
}
}

View File

@ -0,0 +1,88 @@
//
// AuthViewModel.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import Firebase
import SwiftUI
class AuthViewModel: ObservableObject {
@Published var userSession: FirebaseAuth.User?
@Published var didAuthenticateUser = false
@Published var currentUser: User?
private var tempUserSession: FirebaseAuth.User?
private let service = UserService()
init() {
self.userSession = Auth.auth().currentUser
self.fetchUser()
print("DEBUG: user session is \(self.userSession?.uid)")
}
func login(withEmail email: String, password: String) {
print("DEBUG: login with email \(email)")
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to sign in with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.userSession = user
self.fetchUser()
print("DEBUG: Did log user \(user.uid) in.")
}
}
func register(withEmail email: String, password: String, fullname: String, username: String) {
print("DEBUG: register with email \(email)")
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to register with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.tempUserSession = user
let data = ["email": email,
"username": username.lowercased(),
"fullname": fullname]
Firestore.firestore().collection("users")
.document(user.uid)
.setData(data) { _ in
print("DEBUG: setData completion block called...")
self.didAuthenticateUser = true
}
}
}
func signOut() {
userSession = nil
try? Auth.auth().signOut()
}
func uploadProfileImage(_ image: UIImage) {
guard let uid = tempUserSession?.uid else { return }
ImageUploader.uploadImage(image: image) { profileImageUrl in
Firestore.firestore().collection("users")
.document(uid)
.updateData(["profileImageUrl": profileImageUrl]) { _ in
self.userSession = self.tempUserSession
self.fetchUser()
}
}
}
func fetchUser() {
guard let uid = self.userSession?.uid else { return }
service.fetchUser(withUid: uid) { user in
self.currentUser = user
}
}
}

View File

@ -0,0 +1,89 @@
//
// LoginView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct LoginView: View {
@State private var email = ""
@State private var password = ""
@EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack {
AuthHeaderView(title1: "Hello.", title2: "Welcome Back")
VStack(spacing: 40) {
CustomInputField(imageName: "envelope",
palceholderText: "Email",
text: $email)
CustomInputField(imageName: "lock",
palceholderText: "Password",
isSecureField: true,
text: $password)
}
.padding(.horizontal, 32)
.padding(.top, 44)
HStack {
Spacer()
NavigationLink {
Text("Reset password view")
} label: {
Text("Forgot Password?")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color(.systemBlue))
.padding(.top)
.padding(.trailing, 24)
}
}
Button {
viewModel.login(withEmail: email, password: password)
} label: {
Text("Sign In")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340, height: 50)
.background(Color(.systemBlue))
.clipShape(Capsule())
.padding()
}
.shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
Spacer()
NavigationLink {
RegistrationView()
// .navigationBarHidden(true)
.toolbar(.hidden)
} label: {
HStack {
Text("Don't have an account?")
.font(.footnote)
Text("Sign Up")
.font(.footnote)
.fontWeight(.semibold)
}
}
.padding(.bottom, 32)
.foregroundColor(Color(.systemBlue))
}
.ignoresSafeArea()
// .navigationBarHidden(true)
.toolbar(.hidden)
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}

View File

@ -0,0 +1,82 @@
//
// ProfilePhotoSelectorView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct ProfilePhotoSelectorView: View {
@State private var showImagePicker = false
@State private var selectedImage: UIImage?
@State private var profileImage: Image?
@EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack {
AuthHeaderView(title1: "Create your account", title2: "Add a profile photo")
Button {
print("Pick image here..")
showImagePicker.toggle()
} label: {
if let profileImage = profileImage {
profileImage
.resizable()
.modifier(ProfileImageModifier())
} else {
Image(systemName: "plus.circle")
.resizable()
.modifier(ProfileImageModifier())
}
}
.sheet(isPresented: $showImagePicker, onDismiss: loadImage) {
ImagePicker(selectedImage: $selectedImage)
}
.padding(.top, 44)
if let selectedImage = selectedImage {
Button {
print("DEBUG: Finishing register user")
viewModel.uploadProfileImage(selectedImage)
} label: {
Text("Continue")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340, height: 50)
.background(Color(.systemBlue))
.clipShape(Capsule())
.padding()
}
.shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
}
Spacer()
}
.ignoresSafeArea()
}
func loadImage() {
guard let selectedImage = selectedImage else { return }
profileImage = Image(uiImage: selectedImage)
}
}
private struct ProfileImageModifier: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundColor(Color(.systemBlue))
.scaledToFit()
.frame(width: 180, height: 180)
.clipShape(Circle())
}
}
struct ProfilePhotoSelectorView_Previews: PreviewProvider {
static var previews: some View {
ProfilePhotoSelectorView()
}
}

View File

@ -0,0 +1,89 @@
//
// RegistrationView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct RegistrationView: View {
@State private var email = ""
@State private var username = ""
@State private var fullname = ""
@State private var password = ""
// @Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var viewModel: AuthViewModel
var body: some View {
VStack {
AuthHeaderView(title1: "Get started.",
title2: "Create your account")
NavigationLink(destination: ProfilePhotoSelectorView(),
isActive: $viewModel.didAuthenticateUser,
label: {})
VStack(spacing: 40) {
CustomInputField(imageName: "envelope",
palceholderText: "Email",
text: $email)
CustomInputField(imageName: "person",
palceholderText: "Username",
text: $username)
CustomInputField(imageName: "person",
palceholderText: "Full Name",
text: $fullname)
CustomInputField(imageName: "lock",
palceholderText: "Password",
isSecureField: true,
text: $password)
}
.padding(32)
Button {
viewModel.register(withEmail: email,
password: password,
fullname: fullname,
username: username)
} label: {
Text("Sign Up")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340, height: 50)
.background(Color(.systemBlue))
.clipShape(Capsule())
.padding()
}
.shadow(color: .gray.opacity(0.5), radius: 10, x: 0, y: 0)
Spacer()
Button {
dismiss()
} label: {
HStack {
Text("Already have an account?")
.font(.footnote)
Text("Sign In")
.font(.footnote)
.fontWeight(.semibold)
}
}
.padding(.bottom, 32)
}
.ignoresSafeArea()
}
}
struct RegistrationView_Previews: PreviewProvider {
static var previews: some View {
RegistrationView()
}
}

View File

@ -0,0 +1,40 @@
//
// AuthHeaderView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct AuthHeaderView: View {
let title1: String
let title2: String
var body: some View {
VStack(alignment: .leading) {
HStack {Spacer()}
Text(title1)
.font(.largeTitle)
.fontWeight(.semibold)
Text(title2)
.font(.largeTitle)
.fontWeight(.semibold)
}
.frame(height: 260)
.padding(.leading)
.background(Color(.systemBlue))
.foregroundColor(.white)
.clipShape(RoundedShape(corners: [.bottomRight]))
}
}
struct AuthenticationHeader_Previews: PreviewProvider {
static var previews: some View {
AuthHeaderView(title1: "hello,", title2: "world")
}
}

View File

@ -0,0 +1,43 @@
//
// CustomInputField.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct CustomInputField: View {
let imageName: String
let palceholderText: String
var isSecureField: Bool? = false
@Binding var text: String
var body: some View {
VStack {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundColor(Color(.darkGray))
if isSecureField ?? false {
SecureField(palceholderText, text: $text)
} else {
TextField(palceholderText, text: $text)
}
}
Divider()
.background(Color(.darkGray))
}
}
}
struct CustomInputField_Previews: PreviewProvider {
static var previews: some View {
CustomInputField(imageName: "envelope", palceholderText: "Email", text: .constant(""))
}
}

View File

@ -0,0 +1,17 @@
//
// RoundedShape.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct RoundedShape: Shape {
var corners: UIRectCorner
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: 80, height: 80))
return Path(path.cgPath)
}
}

View File

@ -0,0 +1,40 @@
//
// TextArea.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct TextArea: View {
@Binding var text: String
let placeholder: String
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
.padding(4)
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color(.placeholderText))
.padding(.horizontal, 8)
.padding(.vertical, 12)
}
}
.font(.body)
}
}
// struct TextArea_Previews: PreviewProvider {
// static var previews: some View {
// TextArea("preview text")
// }
// }

View File

@ -0,0 +1,34 @@
//
// UserRowView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct UserRowView: View {
var body: some View {
HStack(spacing: 12) {
Circle()
.frame(width: 48, height: 48)
VStack(alignment: .leading, spacing: 4) {
Text("Lith")
.font(.subheadline).bold()
.foregroundColor(.black)
Text("李四")
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
}
.padding(.horizontal)
.padding(.vertical, 4)
}
}
struct UserRowView_Previews: PreviewProvider {
static var previews: some View {
UserRowView()
}
}

View File

@ -0,0 +1,37 @@
//
// UserStatsView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct UserStatsView: View {
var body: some View {
HStack(spacing: 24) {
HStack(spacing: 4) {
Text("13214")
.bold()
.font(.subheadline)
Text("Following")
.font(.caption)
.foregroundColor(.gray)
}
HStack(spacing: 4) {
Text("995353")
.bold()
.font(.subheadline)
Text("Followers")
.font(.caption)
.foregroundColor(.gray)
}
}
}
}
struct UserStatsView_Previews: PreviewProvider {
static var previews: some View {
UserStatsView()
}
}

View File

@ -1,20 +0,0 @@
//
// ExploreView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct ExploreView: View {
var body: some View {
Text("Explore view")
}
}
struct ExploreView_Previews: PreviewProvider {
static var previews: some View {
ExploreView()
}
}

View File

@ -0,0 +1,37 @@
//
// ExploreView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct ExploreView: View {
var body: some View {
NavigationView {
VStack {
ScrollView {
LazyVStack {
ForEach(0...25, id: \.self) { _ in
NavigationLink {
// ProfileView()
} label: {
UserRowView()
}
}
}
}
}
.navigationTitle("Explore")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ExploreView_Previews: PreviewProvider {
static var previews: some View {
ExploreView()
}
}

View File

@ -8,13 +8,35 @@
import SwiftUI
struct FeedView: View {
@State private var showNewTweetView = false
var body: some View {
ScrollView {
LazyVStack {
ForEach(0 ... 20, id: \.self) { _ in
TweetRowView()
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0 ... 20, id: \.self) { _ in
TweetRowView()
}
}
}
Button {
showNewTweetView.toggle()
} label: {
Image(systemName: "square.and.pencil")
.resizable()
.renderingMode(.template)
.frame(width: 28, height: 28)
.padding()
}
.background(Color(.systemBlue))
.foregroundColor(.white)
.clipShape(Circle())
.padding()
.fullScreenCover(isPresented: $showNewTweetView) {
NewTweetView()
}
}
}
}

View File

@ -6,11 +6,20 @@
//
import SwiftUI
import Kingfisher
struct ProfileView: View {
@State private var selectedFilter: TweetFilterViewModel = .tweets
// @Environment(\.presentationMode) var mode // explore
@Environment(\.dismiss) private var dismiss
private let user: User
@Namespace var animation
init(user: User) {
self.user = user
}
var body: some View {
VStack(alignment: .leading) {
headerView
@ -25,12 +34,17 @@ struct ProfileView: View {
Spacer()
}
.toolbar(.hidden)
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView()
ProfileView(user: User(id: NSUUID().uuidString,
username: "zhang3",
fullname: "张三",
profileImageUrl: "",
email: "zhang3@gmail.com"))
}
}
@ -41,16 +55,20 @@ extension ProfileView {
.ignoresSafeArea()
VStack {
Button {
// action here
// mode.wrappedValue.dismiss()
dismiss()
} label: {
Image(systemName: "arrow.left")
.resizable()
.frame(width: 20, height: 16)
.foregroundColor(.white)
.offset(x: 16, y: 12)
.offset(x: 16, y: -4)
}
Circle()
KFImage(URL(string: user.profileImageUrl))
.resizable()
.scaledToFill()
.clipShape(Circle())
.frame(width: 72, height: 72)
.offset(x: 16, y: 24)
}
@ -82,12 +100,12 @@ extension ProfileView {
var userInfoDetails: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Zhang San")
Text(user.fullname)
.font(.title2).bold()
Image(systemName: "checkmark.seal.fill")
.foregroundColor(Color(.systemBlue))
}
Text("@zhang3")
Text("@\(user.username)")
.font(.subheadline)
.foregroundColor(.gray)
Text("一个普通人")
@ -107,25 +125,8 @@ extension ProfileView {
.foregroundColor(.gray)
.font(.caption)
HStack(spacing: 24) {
HStack(spacing: 4) {
Text("13214")
.bold()
.font(.subheadline)
Text("Following")
.font(.caption)
.foregroundColor(.gray)
}
HStack(spacing: 4) {
Text("995353")
.bold()
.font(.subheadline)
Text("Followers")
.font(.caption)
.foregroundColor(.gray)
}
}
.padding(.vertical)
UserStatsView()
.padding(.vertical)
}
.padding(.horizontal)
}

View File

@ -0,0 +1,33 @@
//
// SideMenuViewModel.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import Foundation
enum SideMenuViewModel: Int, CaseIterable {
case profile
case lists
case bookmarks
case logout
var title: String {
switch self {
case .profile: return "Profile"
case .lists: return "Lists"
case .bookmarks: return "Bookmarks"
case .logout: return "Logout"
}
}
var imageName: String {
switch self {
case .profile: return "person"
case .lists: return "list.bullet"
case .bookmarks: return "bookmark"
case .logout: return "arrow.left.square"
}
}
}

View File

@ -0,0 +1,34 @@
//
// SideMenuOptionRowView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct SideMenuOptionRowView: View {
let viewModel: SideMenuViewModel
var body: some View {
HStack(spacing:16) {
Image(systemName: viewModel.imageName)
.font(.headline)
.foregroundColor(.gray)
Text(viewModel.title)
.font(.subheadline)
.foregroundColor(.black)
Spacer()
}
.frame(height: 40)
.padding(.horizontal)
}
}
struct SideMenuOptionRowView_Previews: PreviewProvider {
static var previews: some View {
SideMenuOptionRowView(viewModel: .profile)
}
}

View File

@ -0,0 +1,67 @@
//
// SideMenuView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
import Kingfisher
struct SideMenuView: View {
@EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
if let user = authViewModel.currentUser {
VStack(alignment: .leading, spacing: 32) {
VStack(alignment: .leading) {
KFImage(URL(string: user.profileImageUrl))
.resizable()
.scaledToFill()
.clipShape(Circle())
.frame(width: 48, height: 48)
VStack(alignment: .leading, spacing: 4) {
Text(user.fullname)
.font(.headline)
Text("@\(user.username)")
.font(.caption)
.foregroundColor(.gray)
}
UserStatsView()
.padding(.vertical)
}
.padding(.leading)
ForEach(SideMenuViewModel.allCases, id: \.rawValue) { viewModel in
if viewModel == .profile {
NavigationLink {
ProfileView(user: user)
} label: {
SideMenuOptionRowView(viewModel: viewModel)
}
} else if viewModel == .logout {
Button {
authViewModel.signOut()
} label: {
SideMenuOptionRowView(viewModel: viewModel)
}
} else {
SideMenuOptionRowView(viewModel: viewModel)
}
}
Spacer()
}
}
}
}
struct SideMenuView_Previews: PreviewProvider {
static var previews: some View {
SideMenuView()
}
}

View File

@ -0,0 +1,56 @@
//
// NewTweetView.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct NewTweetView: View {
@State private var caption = ""
// @Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
HStack {
Button {
// presentationMode.wrappedValue.dismiss()
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()
}
}

View File

@ -0,0 +1,16 @@
//
// User.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import FirebaseFirestoreSwift
struct User: Identifiable, Decodable {
@DocumentID var id: String?
let username: String
let fullname: String
let profileImageUrl: String
let email: String
}

View File

@ -0,0 +1,31 @@
//
// ImageUploader.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import Firebase
import FirebaseStorage
import UIKit
struct ImageUploader {
static func uploadImage(image: UIImage, completion: @escaping (String) -> Void) {
guard let imageData = image.jpegData(compressionQuality: 0.5) else { return }
let filename = NSUUID().uuidString
let ref = Storage.storage().reference(withPath: "/profile_image/\(filename)")
ref.putData(imageData, metadata: nil) { _, error in
if let error = error {
print("DEBUG: Failed to upload image with error \(error.localizedDescription)")
return
}
ref.downloadURL { imageUrl, _ in
guard let imageUrl = imageUrl?.absoluteURL else { return }
completion(imageUrl.absoluteString)
}
}
}
}

View File

@ -0,0 +1,24 @@
//
// UserService.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import Firebase
import FirebaseFirestoreSwift
struct UserService {
func fetchUser(withUid uid: String, completion: @escaping(User) -> Void) {
print("DEBUG: Fetching user info..")
Firestore.firestore().collection("users")
.document(uid)
.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)
}
}
}

View File

@ -0,0 +1,44 @@
//
// ImagePicker.swift
// dudu-tweet
//
// Created by ching on 2023/5/2.
//
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
@Environment(\.dismiss) private var dismiss
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.originalImage] as? UIImage else { return }
parent.selectedImage = image
parent.dismiss()
}
}
}

View File

@ -6,12 +6,25 @@
//
import SwiftUI
import Firebase
@main
struct dudu_tweetApp: App {
@StateObject var viewModel = AuthViewModel()
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
MainTabView()
NavigationView {
ContentView()
// ProfilePhotoSelectorView()
}
.environmentObject(viewModel)
}
}
}