diff --git a/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj b/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj index 24521f9..966b4a2 100644 --- a/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj +++ b/dudu-tweet/dudu-tweet.xcodeproj/project.pbxproj @@ -36,6 +36,10 @@ 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 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -66,6 +70,10 @@ 24A59AE92A00F672009C9E3E /* CustomInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInputField.swift; sourceTree = ""; }; 24A59AEC2A00F942009C9E3E /* AuthHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHeaderView.swift; sourceTree = ""; }; 24A59AEE2A010142009C9E3E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../GoogleService-Info.plist"; sourceTree = ""; }; + 24A59AF92A01081F009C9E3E /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; + 24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePhotoSelectorView.swift; sourceTree = ""; }; + 24FA4D9B2A012E1A002D202A /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 24FA4D9D2A0134CB002D202A /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -102,6 +110,7 @@ 2492CC0B2A000EB00086C525 /* dudu-tweet */ = { isa = PBXGroup; children = ( + 24FA4D9A2A012E05002D202A /* Utils */, 2492CC1F2A0022990086C525 /* Extensions */, 2492CC1E2A0022970086C525 /* Model */, 2492CC1D2A0022960086C525 /* Service */, @@ -144,6 +153,7 @@ 2492CC1D2A0022960086C525 /* Service */ = { isa = PBXGroup; children = ( + 24FA4D9D2A0134CB002D202A /* ImageUploader.swift */, ); path = Service; sourceTree = ""; @@ -353,6 +363,7 @@ children = ( 24A59AE32A00EF1F009C9E3E /* LoginView.swift */, 24A59AE52A00EF3A009C9E3E /* RegistrationView.swift */, + 24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */, ); path = Views; sourceTree = ""; @@ -360,6 +371,7 @@ 24A59AE22A00EEF8009C9E3E /* ViewModels */ = { isa = PBXGroup; children = ( + 24A59AF92A01081F009C9E3E /* AuthViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -372,6 +384,14 @@ path = Authentication; sourceTree = ""; }; + 24FA4D9A2A012E05002D202A /* Utils */ = { + isa = PBXGroup; + children = ( + 24FA4D9B2A012E1A002D202A /* ImagePicker.swift */, + ); + path = Utils; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -454,12 +474,15 @@ files = ( 24A59ABE2A003108009C9E3E /* MessagesView.swift in Sources */, 24A59AC22A003249009C9E3E /* ProfileView.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 */, @@ -469,6 +492,7 @@ 24A59AE62A00EF3A009C9E3E /* RegistrationView.swift in Sources */, 24A59ADD2A00DB9F009C9E3E /* NewTweetView.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 */, diff --git a/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate b/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate index 50d2aa3..7c8cda6 100644 Binary files a/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate and b/dudu-tweet/dudu-tweet.xcodeproj/project.xcworkspace/xcuserdata/ching.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/dudu-tweet/dudu-tweet/ContentView.swift b/dudu-tweet/dudu-tweet/ContentView.swift index a3d4513..944c34c 100644 --- a/dudu-tweet/dudu-tweet/ContentView.swift +++ b/dudu-tweet/dudu-tweet/ContentView.swift @@ -9,11 +9,32 @@ import SwiftUI struct ContentView: View { @State private var showMenu = false + @EnvironmentObject var viewModel: AuthViewModel var body: some View { + Group { + // no user loggin + if viewModel.userSession == nil { + LoginView() + } else { + // have a logged in user + mainInterfaceView + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} + + +extension ContentView { + var mainInterfaceView: some View { ZStack(alignment: .topLeading) { MainTabView() -// .navigationBarHidden(showMenu) .toolbar(showMenu ? .hidden : .visible) if showMenu { @@ -52,9 +73,3 @@ struct ContentView: View { } } } - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/dudu-tweet/dudu-tweet/Core/Authentication/ViewModels/AuthViewModel.swift b/dudu-tweet/dudu-tweet/Core/Authentication/ViewModels/AuthViewModel.swift new file mode 100644 index 0000000..ea52931 --- /dev/null +++ b/dudu-tweet/dudu-tweet/Core/Authentication/ViewModels/AuthViewModel.swift @@ -0,0 +1,77 @@ +// +// 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 + private var tempUserSession: FirebaseAuth.User? + + init() { + self.userSession = Auth.auth().currentUser + + 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 + + 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, + "uid": user.uid] + 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 + } + } + } +} diff --git a/dudu-tweet/dudu-tweet/Core/Authentication/Views/LoginView.swift b/dudu-tweet/dudu-tweet/Core/Authentication/Views/LoginView.swift index 74ccb19..6f030dd 100644 --- a/dudu-tweet/dudu-tweet/Core/Authentication/Views/LoginView.swift +++ b/dudu-tweet/dudu-tweet/Core/Authentication/Views/LoginView.swift @@ -11,6 +11,7 @@ struct LoginView: View { @State private var email = "" @State private var password = "" + @EnvironmentObject var viewModel: AuthViewModel var body: some View { VStack { @@ -22,6 +23,7 @@ struct LoginView: View { CustomInputField(imageName: "lock", palceholderText: "Password", + isSecureField: true, text: $password) } .padding(.horizontal, 32) @@ -42,7 +44,7 @@ struct LoginView: View { } Button { - print("Sign in here") + viewModel.login(withEmail: email, password: password) } label: { Text("Sign In") .font(.headline) diff --git a/dudu-tweet/dudu-tweet/Core/Authentication/Views/ProfilePhotoSelectorView.swift b/dudu-tweet/dudu-tweet/Core/Authentication/Views/ProfilePhotoSelectorView.swift new file mode 100644 index 0000000..10de365 --- /dev/null +++ b/dudu-tweet/dudu-tweet/Core/Authentication/Views/ProfilePhotoSelectorView.swift @@ -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() + } +} diff --git a/dudu-tweet/dudu-tweet/Core/Authentication/Views/RegistrationView.swift b/dudu-tweet/dudu-tweet/Core/Authentication/Views/RegistrationView.swift index ed80171..5343369 100644 --- a/dudu-tweet/dudu-tweet/Core/Authentication/Views/RegistrationView.swift +++ b/dudu-tweet/dudu-tweet/Core/Authentication/Views/RegistrationView.swift @@ -15,12 +15,17 @@ struct RegistrationView: View { // @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", @@ -36,12 +41,16 @@ struct RegistrationView: View { CustomInputField(imageName: "lock", palceholderText: "Password", + isSecureField: true, text: $password) } .padding(32) Button { - print("Sign up here") + viewModel.register(withEmail: email, + password: password, + fullname: fullname, + username: username) } label: { Text("Sign Up") .font(.headline) diff --git a/dudu-tweet/dudu-tweet/Core/Components/CustomInputField.swift b/dudu-tweet/dudu-tweet/Core/Components/CustomInputField.swift index e4b07b5..2b83a7d 100644 --- a/dudu-tweet/dudu-tweet/Core/Components/CustomInputField.swift +++ b/dudu-tweet/dudu-tweet/Core/Components/CustomInputField.swift @@ -10,6 +10,7 @@ import SwiftUI struct CustomInputField: View { let imageName: String let palceholderText: String + var isSecureField: Bool? = false @Binding var text: String @@ -21,8 +22,12 @@ struct CustomInputField: View { .scaledToFit() .frame(width: 20, height: 20) .foregroundColor(Color(.darkGray)) + if isSecureField ?? false { + SecureField(palceholderText, text: $text) + } else { + TextField(palceholderText, text: $text) + } - TextField(palceholderText, text: $text) } Divider() diff --git a/dudu-tweet/dudu-tweet/Core/SideMenu/Views/SideMenuView.swift b/dudu-tweet/dudu-tweet/Core/SideMenu/Views/SideMenuView.swift index 9805826..e9b2d91 100644 --- a/dudu-tweet/dudu-tweet/Core/SideMenu/Views/SideMenuView.swift +++ b/dudu-tweet/dudu-tweet/Core/SideMenu/Views/SideMenuView.swift @@ -8,6 +8,9 @@ import SwiftUI struct SideMenuView: View { + + @EnvironmentObject var authViewModel: AuthViewModel + var body: some View { VStack(alignment: .leading, spacing: 32) { VStack(alignment: .leading) { @@ -36,7 +39,7 @@ struct SideMenuView: View { } } else if viewModel == .logout { Button { - print("handle logout here") + authViewModel.signOut() } label: { SideMenuOptionRowView(viewModel: viewModel) } diff --git a/dudu-tweet/dudu-tweet/Service/ImageUploader.swift b/dudu-tweet/dudu-tweet/Service/ImageUploader.swift new file mode 100644 index 0000000..0d6b4a5 --- /dev/null +++ b/dudu-tweet/dudu-tweet/Service/ImageUploader.swift @@ -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) + } + } + } +} diff --git a/dudu-tweet/dudu-tweet/Utils/ImagePicker.swift b/dudu-tweet/dudu-tweet/Utils/ImagePicker.swift new file mode 100644 index 0000000..bb496f5 --- /dev/null +++ b/dudu-tweet/dudu-tweet/Utils/ImagePicker.swift @@ -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() + } + } +} diff --git a/dudu-tweet/dudu-tweet/dudu_tweetApp.swift b/dudu-tweet/dudu-tweet/dudu_tweetApp.swift index 3b2e96d..97fc141 100644 --- a/dudu-tweet/dudu-tweet/dudu_tweetApp.swift +++ b/dudu-tweet/dudu-tweet/dudu_tweetApp.swift @@ -11,6 +11,9 @@ import Firebase @main struct dudu_tweetApp: App { + @StateObject var viewModel = AuthViewModel() + + init() { FirebaseApp.configure() } @@ -19,8 +22,9 @@ struct dudu_tweetApp: App { WindowGroup { NavigationView { ContentView() -// LoginView() +// ProfilePhotoSelectorView() } + .environmentObject(viewModel) } } }