diff --git a/EngAttack.xcodeproj/project.pbxproj b/EngAttack.xcodeproj/project.pbxproj index 59df91d..41ded31 100644 --- a/EngAttack.xcodeproj/project.pbxproj +++ b/EngAttack.xcodeproj/project.pbxproj @@ -32,11 +32,12 @@ CDC208852BE3AFA200105E63 /* SignViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC2087F2BE3AFA200105E63 /* SignViewModel.swift */; }; CDC208872BE3AFC500105E63 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC208862BE3AFC500105E63 /* RootView.swift */; }; CDC2088B2BE3BEB800105E63 /* CountDownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC2088A2BE3BEB800105E63 /* CountDownView.swift */; }; + D875BD092BE7DDD200B254CD /* StorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875BD082BE7DDD200B254CD /* StorageManager.swift */; }; + D875BD0C2BE7E00D00B254CD /* ProfoleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875BD0B2BE7E00D00B254CD /* ProfoleViewModel.swift */; }; + D875BD102BE7ED1000B254CD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D875BD0F2BE7ED1000B254CD /* GoogleService-Info.plist */; }; D8BE454C2BE496FD00C9BD40 /* BookMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE454B2BE496FD00C9BD40 /* BookMark.swift */; }; D8BE455B2BE4BD2B00C9BD40 /* SignInState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE455A2BE4BD2B00C9BD40 /* SignInState.swift */; }; - D8FEE2F82BE538D5001FC45C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D8FEE2F72BE538D5001FC45C /* GoogleService-Info.plist */; }; D8FEE2FB2BE5BF1D001FC45C /* ProfileSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FEE2FA2BE5BF1D001FC45C /* ProfileSettingView.swift */; }; - D8FEE2FD2BE5F2F3001FC45C /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FEE2FC2BE5F2F3001FC45C /* ImagePicker.swift */; }; D8FEE3002BE60F21001FC45C /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = D8FEE2FF2BE60F21001FC45C /* FirebaseAuth */; }; D8FEE3022BE60F21001FC45C /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = D8FEE3012BE60F21001FC45C /* FirebaseFirestore */; }; D8FEE3042BE60F21001FC45C /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D8FEE3032BE60F21001FC45C /* FirebaseFirestoreSwift */; }; @@ -77,12 +78,12 @@ CDC2087F2BE3AFA200105E63 /* SignViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignViewModel.swift; sourceTree = ""; }; CDC208862BE3AFC500105E63 /* RootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; CDC2088A2BE3BEB800105E63 /* CountDownView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountDownView.swift; sourceTree = ""; }; + D875BD082BE7DDD200B254CD /* StorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageManager.swift; sourceTree = ""; }; + D875BD0B2BE7E00D00B254CD /* ProfoleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfoleViewModel.swift; sourceTree = ""; }; + D875BD0F2BE7ED1000B254CD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../Downloads/GoogleService-Info.plist"; sourceTree = ""; }; D8BE454B2BE496FD00C9BD40 /* BookMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookMark.swift; sourceTree = ""; }; D8BE455A2BE4BD2B00C9BD40 /* SignInState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInState.swift; sourceTree = ""; }; - D8FEE2F72BE538D5001FC45C /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../Downloads/GoogleService-Info.plist"; sourceTree = ""; }; - D8FEE2F92BE538DA001FC45C /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; D8FEE2FA2BE5BF1D001FC45C /* ProfileSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingView.swift; sourceTree = ""; }; - D8FEE2FC2BE5F2F3001FC45C /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; D8FEE3072BE61F91001FC45C /* Rank.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rank.swift; sourceTree = ""; }; F7A23E552BE1DB7E00A044BD /* TabViewSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewSetting.swift; sourceTree = ""; }; F7A23E572BE1DE1200A044BD /* RankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingView.swift; sourceTree = ""; }; @@ -120,7 +121,6 @@ AFBC3A232BE0D5E400CE7542 = { isa = PBXGroup; children = ( - D8FEE2F72BE538D5001FC45C /* GoogleService-Info.plist */, AFBC3A2E2BE0D5E500CE7542 /* EngAttack */, AFBC3A2D2BE0D5E500CE7542 /* Products */, ); @@ -140,8 +140,8 @@ AFBC3A332BE0D5E700CE7542 /* Assets.xcassets */, AF9552D72BE1D08E0019A124 /* Sounds */, F7A23E552BE1DB7E00A044BD /* TabViewSetting.swift */, + D875BD0F2BE7ED1000B254CD /* GoogleService-Info.plist */, F7F07C842BE0DF96009A75A9 /* GameStartView.swift */, - D8FEE2F92BE538DA001FC45C /* GoogleService-Info.plist */, CD37DEF82BE0D7E000F7CB39 /* ContentViewViewModel.swift */, CDC2088A2BE3BEB800105E63 /* CountDownView.swift */, BFD6A8DF2BE207E10009BFE2 /* ContentView.swift */, @@ -149,9 +149,8 @@ BF755ADD2BE1D792007DD8AE /* Launch Screen.swift */, BF36CBE32BE118E90089D070 /* EngAttackApp.swift */, AF62BD442BE0DDF700A88EDE /* SettingView.swift */, - D8FEE2FA2BE5BF1D001FC45C /* ProfileSettingView.swift */, AFB5BF0A2BE3D7A80005932D /* SettingViewModel.swift */, - D8FEE2FC2BE5F2F3001FC45C /* ImagePicker.swift */, + D875BD0A2BE7DFF000B254CD /* Profile */, D8BE454F2BE4976400C9BD40 /* Rank */, CDC208802BE3AFA200105E63 /* Sign */, BF0C51612BE325D700A10946 /* Dictionary */, @@ -214,6 +213,16 @@ path = Sign; sourceTree = ""; }; + D875BD0A2BE7DFF000B254CD /* Profile */ = { + isa = PBXGroup; + children = ( + D875BD082BE7DDD200B254CD /* StorageManager.swift */, + D8FEE2FA2BE5BF1D001FC45C /* ProfileSettingView.swift */, + D875BD0B2BE7E00D00B254CD /* ProfoleViewModel.swift */, + ); + path = Profile; + sourceTree = ""; + }; D8BE454F2BE4976400C9BD40 /* Rank */ = { isa = PBXGroup; children = ( @@ -300,7 +309,7 @@ buildActionMask = 2147483647; files = ( F7A23E5D2BE1DF8500A044BD /* SOYO Maple Regular.otf in Resources */, - D8FEE2F82BE538D5001FC45C /* GoogleService-Info.plist in Resources */, + D875BD102BE7ED1000B254CD /* GoogleService-Info.plist in Resources */, F7A23E5C2BE1DF8500A044BD /* SOYO Maple Bold.otf in Resources */, AFBC3A372BE0D5E700CE7542 /* Preview Assets.xcassets in Resources */, AF9552DD2BE1D09F0019A124 /* background.wav in Resources */, @@ -322,6 +331,8 @@ AF05DFF42BE28C7D005625E0 /* WordManager.swift in Sources */, CDC208812BE3AFA200105E63 /* AuthenticationManager.swift in Sources */, D8BE455B2BE4BD2B00C9BD40 /* SignInState.swift in Sources */, + D875BD092BE7DDD200B254CD /* StorageManager.swift in Sources */, + D875BD0C2BE7E00D00B254CD /* ProfoleViewModel.swift in Sources */, BFD6A8E12BE209CF0009BFE2 /* Launch Screen.swift in Sources */, CDA4363E2BE28E9B0047E2C0 /* WordDictionaryViewModel.swift in Sources */, D8FEE3082BE61F91001FC45C /* Rank.swift in Sources */, @@ -334,7 +345,6 @@ CDC2088B2BE3BEB800105E63 /* CountDownView.swift in Sources */, CDC208822BE3AFA200105E63 /* Field.swift in Sources */, CDA4363D2BE28E9B0047E2C0 /* WordBookmarkView.swift in Sources */, - D8FEE2FD2BE5F2F3001FC45C /* ImagePicker.swift in Sources */, CD1DCC042BE52AFB0061357B /* WordBookmarkViewModel.swift in Sources */, F7A23E582BE1DE1200A044BD /* RankingView.swift in Sources */, D8BE454C2BE496FD00C9BD40 /* BookMark.swift in Sources */, diff --git a/EngAttack/Bookmark/WordBookmarkView.swift b/EngAttack/Bookmark/WordBookmarkView.swift index ebc4afa..22df8cf 100644 --- a/EngAttack/Bookmark/WordBookmarkView.swift +++ b/EngAttack/Bookmark/WordBookmarkView.swift @@ -62,7 +62,6 @@ struct WordBookmarkView: View { guard let document = doc else { return } if document.exists { guard let words = document.data(), let list = words["List"] as? [[String: Any]] else { return } - for item in list { guard let word = item["word"] as? String else { continue } guard let description = item["description"] as? String else { continue } diff --git a/EngAttack/ContentView.swift b/EngAttack/ContentView.swift index e7436bd..b6fdaee 100644 --- a/EngAttack/ContentView.swift +++ b/EngAttack/ContentView.swift @@ -98,7 +98,7 @@ struct ContentView: View { SoundSetting.instance.playSound(sound: .background) } .onDisappear { - addRank(names:singViewModel.name, scores:contentViewModel.score) + // addRank(names:singViewModel.name, scores:contentViewModel.score) contentViewModel.stopTimer() SoundSetting.instance.stopMusic() } diff --git a/EngAttack/GameStartView.swift b/EngAttack/GameStartView.swift index 447f6d8..c237c96 100644 --- a/EngAttack/GameStartView.swift +++ b/EngAttack/GameStartView.swift @@ -37,7 +37,7 @@ struct GameStartView: View { .foregroundStyle(.green) Picker(contentViewModel.isKR ? "Choose time" : "선택 시간", selection: $selectedTime) { ForEach(times, id: \.self) { time in - Text(String(format: "%.0f", Double(time)) + "\(contentViewModel.isKR ? "S" : "초")").tag(time) + Text(String(format: "%.0f", Double(time)) + "\(contentViewModel.isKR ? "s" : "초")").tag(time) .foregroundStyle(Color("PickerFontColor")) } } diff --git a/EngAttack/ImagePicker.swift b/EngAttack/ImagePicker.swift deleted file mode 100644 index 04729d4..0000000 --- a/EngAttack/ImagePicker.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ImagePicker.swift -// EngAttack -// -// Created by mosi on 5/4/24. -// - -import Foundation -import SwiftUI - -struct ImagePicker: UIViewControllerRepresentable { - - @Binding var image: UIImage? - @Environment(\.presentationMode) var mode - - func makeCoordinator() -> Coordinator { - 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.image = image - parent.mode.wrappedValue.dismiss() - } - } -} diff --git a/EngAttack/Profile/ProfileSettingView.swift b/EngAttack/Profile/ProfileSettingView.swift new file mode 100644 index 0000000..47f80a5 --- /dev/null +++ b/EngAttack/Profile/ProfileSettingView.swift @@ -0,0 +1,209 @@ +// +// ProfileSettingView.swift +// EngAttack +// +// Created by mosi on 5/4/24. +// + +import SwiftUI +import PhotosUI +import FirebaseAuth +import FirebaseStorage +import FirebaseFirestore + +struct ProfileSettingView: View { + @EnvironmentObject var contentViewModel : ContentViewViewModel + @StateObject var signViewModel : SignViewModel + @StateObject var profileViewModel = ProfileViewModel() + @State var showImagePicker = false + @State var selectedUIImage: PhotosPickerItem? = nil + @State var image: UIImage? + @Binding var url : URL? + @FocusState private var focusedField: Field? + @Binding var name : String + @Binding var email : String + @State private var messageString: String = "" + @State private var password : String = "" + @State private var pwdisShowing = false + @State private var isValidPasswords: Bool = false + @State private var isError: Bool = false + @Binding var isUpdateDone : Bool + @State var isAlert : Bool = false + @State var isDone : Bool = false + + + + + var body: some View { + NavigationStack { + Form { + Section { + PhotosPicker(selection:$selectedUIImage, matching: .images, photoLibrary: .shared()) { + if let url { + AsyncImage(url: url) { phase in + if let image = phase.image { + image + .resizable() + .clipShape(Circle()) + .frame(width: 150, height: 150) + .padding(.leading ,80) + } + else if phase.error != nil{ + ProgressView() + .frame(width: 150, height: 150) + .padding(.leading, 80) + } else { + ProgressView() + .frame(width: 150, height: 150) + .padding(.leading, 80) + } + } + } else { + Image(systemName: "person.circle") + .resizable() + .foregroundColor(.blue) + .frame(width: 150, height: 150) + .padding(.leading ,80) + } + } + .onChange(of: selectedUIImage, perform: { newValue in + if let newValue { + profileViewModel.saveProfileImage(item: selectedUIImage!, email: email, names: name) + } + + }) + .task{ + do { + try await profileViewModel.loadCurrentUser() + let path = "\(signViewModel.uid).jpeg" + let urls = try await StorageManager.shared.getUrlForImage(path:path) + self.url = urls + } catch let error { + print(error) + } + } + + } + .listRowBackground(Color.clear) + Section(header: Text(contentViewModel.isKR ? "Name" : "이름") + .font(.system(size: 15)) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + .bold()) { + TextField("", text: $name) + .keyboardType(.emailAddress) + .font(.system(size: 17)) + .textInputAutocapitalization(.never) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + .focused($focusedField, equals: .name) + .onSubmit { focusedField = .password } + } + Section(header: Text(contentViewModel.isKR ? "Email" : "이메일") + .font(.system(size: 15)) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + .bold()) { + Text(email) + .font(.system(size: 17)) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + } + + Section(header: Text(contentViewModel.isKR ? "Password" : "비밀번호") + .font(.system(size: 15)) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + .bold()) { + if pwdisShowing { + SecureField("", text: $password) + .font(.system(size: 17)) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + .focused($focusedField, equals: .password) + .onSubmit { focusedField = nil } + } else { + TextField("", text: $password) + .font(.system(size: 17)) + .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) + .focused($focusedField, equals: .password) + .onSubmit { focusedField = nil } + } + } + Section { + Button { + pwdisShowing.toggle() + } label: { + if pwdisShowing { + Text(contentViewModel.isKR ? "Show password" : "패스워드 보기" ) + .frame(width: 150, height: 35) + .foregroundStyle(.primary) + } + else { + Text(contentViewModel.isKR ? "Hide password" : "패스워드 가리기") + .frame(width: 150, height: 35) + .foregroundStyle(.primary) + } + + } + .accentColor(pwdisShowing ? .blue : .gray) + .padding(.horizontal, 100) + .buttonStyle(.borderedProminent) + Button { + Task { + do { + try await signViewModel.updatePassword(password: password) + isAlert = true + isDone = true + let db = Firestore.firestore() + try await db.collection("USER").document(signViewModel.uid).updateData(["name" : name]) + messageString = contentViewModel.isKR ? "\(signViewModel.name)s information has been update. please sign in again" : "\(signViewModel.name)의 정보가 변경되었습니다. 다시 로그인 해주세요." + signViewModel.uid = "" + signViewModel.email = "" + signViewModel.name = "" + try await signViewModel.signOut() + signViewModel.Signstate = .signedOut + return + } catch { + isValidPasswords = isValidPassword(pwd: password) + if !isValidPasswords { + messageString = contentViewModel.isKR ? "The password must be at least 8 uppercase characters long" : "패스워드는 대소문자 8자리 이상이어야 합니다" + } + messageString = contentViewModel.isKR ? "An unknown error occurred. Please try again." : "알수 없는 오류가 발생했습니다. 다시 시도해주세요." + + isAlert = true + isError = true + } + } + }label: { + Text(contentViewModel.isKR ? "Update" : "수정하기") + .frame(width: 150, height: 35) + } + .alert(isPresented: $isAlert) { + Alert(title: Text(contentViewModel.isKR ? "Notification" : "알림"), message: Text(messageString), dismissButton: .default(Text(contentViewModel.isKR ? "Done" : "확인"), action: { + if isError { + isAlert = false + isError = false + isDone = false + } + else if isDone { + isUpdateDone = false + } + })) + } + .disabled(name.isEmpty) + .padding(.horizontal, 100) + .buttonStyle(.borderedProminent) + } + .listRowBackground(Color.clear) + } + } + } + +} + +extension ProfileSettingView { + + //비밀번호 형식 검사 -> 소문자, 대문자, 숫자 8자리 이상 + func isValidPassword(pwd: String) -> Bool { + let passwordRegex = "^[a-zA-Z0-9]{8,}$" + return NSPredicate(format: "SELF MATCHES %@", passwordRegex).evaluate(with: pwd) + } +} + + + diff --git a/EngAttack/Profile/ProfoleViewModel.swift b/EngAttack/Profile/ProfoleViewModel.swift new file mode 100644 index 0000000..15a0c1f --- /dev/null +++ b/EngAttack/Profile/ProfoleViewModel.swift @@ -0,0 +1,34 @@ +// +// ProfoleViewModel.swift +// EngAttack +// +// Created by mosi on 5/6/24. +// + +import Foundation +import SwiftUI +import PhotosUI +import FirebaseAuth +import Firebase + +@MainActor +final class ProfileViewModel: ObservableObject { + @Published private(set) var user: AuthDataResultModel? = nil + + func loadCurrentUser() throws { + self.user = try AuthenticationManager.shared.getAuthenticatedUser() + } + + func saveProfileImage(item: PhotosPickerItem, email: String, names: String){ + Task { + guard let data = try await item.loadTransferable(type: Data.self) else { return } + let (path, name) = try await StorageManager.shared.saveImage(data: data, userID: Auth.auth().currentUser!.uid ) + print("Success!") + print(path) + print(name) + try await StorageManager.shared.updateUserProfileImagePath(userId: Auth.auth().currentUser!.uid, email: email, name: names, path: name) + + } + } + +} diff --git a/EngAttack/Profile/StorageManager.swift b/EngAttack/Profile/StorageManager.swift new file mode 100644 index 0000000..0599fc2 --- /dev/null +++ b/EngAttack/Profile/StorageManager.swift @@ -0,0 +1,76 @@ +// +// StorageManager.swift +// EngAttack +// +// Created by mosi on 5/6/24. +// + +import Foundation +import FirebaseStorage +import FirebaseAuth +import FirebaseFirestore +import UIKit + + +final class StorageManager { + + static let shared = StorageManager() + private init() { } + + private let storage = Storage.storage().reference() + + private var imagesReference: StorageReference { + storage.child("images") + } + + private func userReference(userID: String) -> StorageReference { + storage + } + + func getUrlForImage(path: String) async throws -> URL { + try await Storage.storage().reference(withPath: path).downloadURL() + } + + func updateUserProfileImagePath(userId: String, email: String, name: String ,path: String) async throws { + let db = Firestore.firestore() + try await db.collection("USER").document(userId).updateData(["email" : email, "name" : name, "photoUrl" : path]) + } + + + func saveImage(data: Data, userID: String) async throws ->(path: String, name: String) { + let meta = StorageMetadata() + meta.contentType = "image/jpeg" + + let path = "\(userID).jpeg" + let returnedMetaData = try await userReference(userID: userID).child(path).putDataAsync(data, metadata: meta) + + guard let returnedPath = returnedMetaData.path, let returnedName = returnedMetaData.name else { + throw URLError(.badServerResponse) + } + + return (returnedPath, returnedName) + } + + func saveImage(image: UIImage, userId: String) async throws -> (path: String, name: String) { + // image.pngData() 이미지 Png면 이렇게 쓰기 + guard let data = image.jpegData(compressionQuality: 1) else { + throw URLError(.backgroundSessionWasDisconnected) + } + + return try await saveImage(image: image, userId: userId) + } + + + func getData(userID: String, path:String) async throws -> Data { + try await userReference(userID: userID).child(path).data(maxSize: 3 * 1024 * 1024) + } + + + func getImage(userID: String, path: String) async throws -> UIImage { + let data = try await getData (userID: userID, path: path) + guard let image = UIImage(data: data) else { + throw URLError(.badServerResponse) + } + return image + } +} diff --git a/EngAttack/ProfileSettingView.swift b/EngAttack/ProfileSettingView.swift deleted file mode 100644 index 9e52ab6..0000000 --- a/EngAttack/ProfileSettingView.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// ProfileSettingView.swift -// EngAttack -// -// Created by mosi on 5/4/24. -// - -import SwiftUI - -struct ProfileSettingView: View { - @EnvironmentObject var contentViewModel : ContentViewViewModel - @State var showImagePicker = false - @State var selectedUIImage: UIImage? - @State var image: Image? - @FocusState private var focusedField: Field? - @Binding var isprofileLoad : Bool - @State private var name : String = "" - @State private var email : String = "" - @State private var password : String = "" - @State private var pwdisShowing = false - - func loadImage() { - guard let selectedImage = selectedUIImage else { return } - image = Image(uiImage: selectedImage) - } - - var body: some View { - NavigationStack { - Form { - Section { - Button { - showImagePicker.toggle() - } label: { - if let image = image { - image - .resizable() - .clipShape(Circle()) - .frame(width: 100, height: 100) - } else { - Image(systemName: "person.circle") - .resizable() - .foregroundColor(.blue) - .frame(width: 100, height: 100) - } - - - - } - .listRowBackground(Color.clear) - .padding(.horizontal, 100) - .sheet(isPresented: $showImagePicker, onDismiss: { - loadImage() - }) { - ImagePicker(image: $selectedUIImage) - } - } - Section(header: Text(contentViewModel.isKR ? "Name" : "이름") - .font(.system(size: 15)) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .bold()) { - TextField("", text: $name) - .keyboardType(.emailAddress) - .font(.system(size: 17)) - .textInputAutocapitalization(.never) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .focused($focusedField, equals: .name) - .onSubmit { focusedField = .id } - } - - Section(header: Text(contentViewModel.isKR ? "Email" : "이메일") - .font(.system(size: 15)) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .bold()) { - TextField("", text: $email) - .keyboardType(.emailAddress) - .font(.system(size: 17)) - .textInputAutocapitalization(.never) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .focused($focusedField, equals: .id) - .onSubmit { focusedField = .password } - } - - Section(header: Text(contentViewModel.isKR ? "Password" : "비밀번호") - .font(.system(size: 15)) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .bold()) { - if pwdisShowing { - SecureField("", text: $password) - .font(.system(size: 17)) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .focused($focusedField, equals: .password) - .onSubmit { focusedField = nil } - } else { - TextField("", text: $password) - .font(.system(size: 17)) - .foregroundStyle(contentViewModel.isDarkMode ? .white : .black) - .focused($focusedField, equals: .password) - .onSubmit { focusedField = nil } - } - } - Section { - Button { - pwdisShowing.toggle() - } label: { - if pwdisShowing { - Text(contentViewModel.isKR ? "Show password" : "패스워드 보기" ) - .frame(width: 150, height: 35) - .foregroundStyle(.primary) - } - else { - Text(contentViewModel.isKR ? "Hide password" : "패스워드 가리기") - .frame(width: 150, height: 35) - .foregroundStyle(.primary) - } - - } - .accentColor(pwdisShowing ? .blue : .gray) - .padding(.horizontal, 100) - .buttonStyle(.borderedProminent) - Button { - - - } label: { - Text(contentViewModel.isKR ? "Submit" : "변경하기") - .frame(width: 100, height: 35) - - } - .disabled(name.isEmpty) - .padding(.horizontal, 100) - .buttonStyle(.borderedProminent) - } - .listRowBackground(Color.clear) - } - } - } - -} - - - diff --git a/EngAttack/RootView.swift b/EngAttack/RootView.swift index 865aa49..4e9dd0e 100644 --- a/EngAttack/RootView.swift +++ b/EngAttack/RootView.swift @@ -11,7 +11,7 @@ import FirebaseCore import FirebaseAuth struct RootView: View { - @StateObject var signViewModel : SignViewModel = SignViewModel() + @StateObject var signViewModel:SignViewModel = SignViewModel() var body: some View { VStack { if signViewModel.Signstate == .signedIn { @@ -22,12 +22,12 @@ struct RootView: View { } else { SignInView(signViewModel: signViewModel) .environmentObject(ContentViewViewModel()) + .environmentObject(SignViewModel()) } } .onAppear { - if signViewModel.currentUser != nil && signViewModel.uid != "" { + if signViewModel.currentUser != nil { signViewModel.Signstate = .signedIn - print("테스트") } } } diff --git a/EngAttack/SettingView.swift b/EngAttack/SettingView.swift index 68b355c..f221a4c 100644 --- a/EngAttack/SettingView.swift +++ b/EngAttack/SettingView.swift @@ -8,14 +8,21 @@ import SwiftUI import AVKit import FirebaseAuth +import FirebaseFirestore +import FirebaseStorage struct SettingView: View { @EnvironmentObject var contentsViewModel: ContentViewViewModel @EnvironmentObject var setViewModel: SettingViewModel - @StateObject var signViewModel : SignViewModel = SignViewModel() + @StateObject var signViewModel : SignViewModel @State var settingsSound = false - @State var isprofileLoad = false + @State var isUpdateDone = false @State var isUnregister = false + @State var isSignOut = false + @State var url : URL? + @State var name = "" + @State var email = "" + //@State var backVolume = 0.0 let effectVol = 0.3 @@ -29,22 +36,48 @@ struct SettingView: View { // MARK: 마이페이지 Section(header: Text(contentsViewModel.isKR ? "Mypage" : "마이페이지").font(.system(size: 18))) { Button { - self.isprofileLoad = true + name = signViewModel.name + email = signViewModel.email + self.isUpdateDone = true } label: { HStack { - Image(systemName: "person.circle") - .resizable() - .aspectRatio(contentMode: .fit) + if let url { + AsyncImage(url: url) { phase in + if let image = phase.image { + image + .resizable() + .clipShape(Circle()) + .frame(width: 80, height: 80) + .padding(.leading ,3) + } + else if phase.error != nil{ + ProgressView() + .padding(.leading) + .frame(width: 80, height: 80) + } else { + ProgressView() + .padding(.leading) + .frame(width: 80, height: 80) + } + } + } else { + Image(systemName: "person.circle") + .resizable() + .foregroundColor(.blue) + .frame(width: 80, height: 80) + .padding(.leading ,3) + } + Text(signViewModel.name) .foregroundStyle(contentsViewModel.isDarkMode ? .white : .black) .padding(.leading, 20) .fontWeight(.semibold) - .font(.system(size: 25)) - + .font(.system(size: 30)) + } .frame(height: 70) - .sheet(isPresented: $isprofileLoad) { - ProfileSettingView(isprofileLoad: $isprofileLoad) + .sheet(isPresented: $isUpdateDone) { + ProfileSettingView(signViewModel: SignViewModel(), url: $url, name:$name, email: $email, isUpdateDone: $isUpdateDone) } } } @@ -93,19 +126,30 @@ struct SettingView: View { } .frame(height: 25) } - Button { - Task { - do{ - try await signViewModel.signOut() - signViewModel.Signstate = .signedOut + .alert(isPresented: $isUnregister) { + Alert(title: Text(contentsViewModel.isKR ? "Warning" : "경고"), + message: Text(contentsViewModel.isKR ? "Do you really want to cancel your membership?" : "정말로 회원탈퇴 하시겠습니까?"), + primaryButton: .default(Text(contentsViewModel.isKR ? "Cancel" : "취소하기"), action: { + isUnregister = false + }), + secondaryButton: .destructive(Text(contentsViewModel.isKR ? "Delete" : "삭제"), action: { + Task { + let db = Firestore.firestore() + guard let userID = Auth.auth().currentUser?.uid else { return } + try await signViewModel.deleteUser() + try await db.collection("USER").document(userID).delete() + try await db.collection("Rank").document(userID).delete() + try await db.collection("BookMark").document(userID).delete() + try await Storage.storage().reference().child("\(userID).jpeg").delete() signViewModel.uid = "" signViewModel.name = "" - print(signViewModel.Signstate) - return - } catch let error { - print(error) + signViewModel.email = "" + signViewModel.Signstate = .signedOut } - } + })) + } + Button { + isSignOut = true } label: { HStack { Text(contentsViewModel.isKR ? "Logout" : "로그아웃") @@ -113,28 +157,40 @@ struct SettingView: View { .padding(.leading, 5) .font(.system(size: 21)) } + .alert(isPresented: $isSignOut) { + Alert(title: Text(contentsViewModel.isKR ? "Warning" : "경고"), + message: Text(contentsViewModel.isKR ? "Do you want really sign out?" : "정말로 로그아웃 하시겠습니까?"), + primaryButton: .default(Text(contentsViewModel.isKR ? "Cancel" : "취소하기"), action: { + isSignOut = false + }), + secondaryButton: .destructive(Text(contentsViewModel.isKR ? "Done" : "확인"), action: { + Task { + do{ + try await signViewModel.signOut() + signViewModel.Signstate = .signedOut + signViewModel.uid = "" + signViewModel.email = "" + signViewModel.name = "" + return + } catch let error { + print(error) + } + } + })) + } .frame(height: 25) } } } } - .alert(isPresented: $isUnregister) { - Alert(title: Text(contentsViewModel.isKR ? "Warning" : "경고"), - message: Text(contentsViewModel.isKR ? "Do you really want to cancel your membership?" : "정말로 회원탈퇴 하시겠습니까?"), - primaryButton: .default(Text(contentsViewModel.isKR ? "Cancel" : "취소하기"), action: { - - }), - secondaryButton: .destructive(Text(contentsViewModel.isKR ? "Delete" : "삭제"), action: { - - })) - } + .font(.system(size: 20)) } } #Preview { - SettingView() + SettingView(signViewModel: SignViewModel()) .environmentObject(ContentViewViewModel()) .environmentObject(SettingViewModel()) } diff --git a/EngAttack/Sign/AuthenticationManager.swift b/EngAttack/Sign/AuthenticationManager.swift index 7d8333f..6561799 100644 --- a/EngAttack/Sign/AuthenticationManager.swift +++ b/EngAttack/Sign/AuthenticationManager.swift @@ -51,6 +51,13 @@ final class AuthenticationManager { try Auth.auth().signOut() } + func updatePassword(passwords: String) async throws { + try await Auth.auth().currentUser?.updatePassword(to: passwords) + } + + func deleteUser() async throws { + try await Auth.auth().currentUser?.delete() + } diff --git a/EngAttack/Sign/SignInView.swift b/EngAttack/Sign/SignInView.swift index 506018d..b01c992 100644 --- a/EngAttack/Sign/SignInView.swift +++ b/EngAttack/Sign/SignInView.swift @@ -12,9 +12,10 @@ import FirebaseFirestore struct SignInView: View { @EnvironmentObject var contentViewModel: ContentViewViewModel - + @StateObject var profileViewModel = ProfileViewModel() @StateObject var signViewModel : SignViewModel @EnvironmentObject var setViewModel: SettingViewModel + @State private var correctLogin = false @State private var email = "" @State private var password = "" @@ -54,13 +55,13 @@ struct SignInView: View { Button { Task { do { - signViewModel.email = email - signViewModel.password = password - try await signViewModel.signIn() + try await signViewModel.signIn(email: email, password: password) + try await signViewModel.loadName() guard let userID = Auth.auth().currentUser?.uid else { return } signViewModel.uid = userID + signViewModel.email = email + signViewModel.password = password signViewModel.Signstate = .signedIn - loadName(userID: userID) loginActive = true return } catch { @@ -113,26 +114,8 @@ extension SignInView { return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email) } - func loadName(userID :String) { - var names = "" - let db = Firestore.firestore() - db.collection("USER").document(userID).getDocument { (doc, error) in - guard error == nil else { - print("error", error ?? "") - return - } - if let doc = doc, doc.exists { - let data = doc.data() - if let data = data { - let name = data["name"] as? String ?? "" - if signViewModel.name == "" { - signViewModel.name = name - return - } - - } - } - } - - } + + + + } diff --git a/EngAttack/Sign/SignUpView.swift b/EngAttack/Sign/SignUpView.swift index 0a33d27..efdbf4d 100644 --- a/EngAttack/Sign/SignUpView.swift +++ b/EngAttack/Sign/SignUpView.swift @@ -19,8 +19,9 @@ struct SignUpView: View { @State private var isValidPasswords: Bool = false @State private var emailCheck: Bool = false @State private var isError: Bool = false - @Binding var isSignUpActive : Bool - @State var isDone : Bool = false + @Binding var isSignUpActive : Bool + @State var isAlertActive = false + @State var isDoneActive = false @State private var messageString: String = "" @FocusState private var focusedField: Field? @@ -64,29 +65,26 @@ struct SignUpView: View { .focused($focusedField, equals: .password) .onSubmit { focusedField = nil } } - Section { Button { Task { do { - signViewModel.email = email - signViewModel.password = password - try await signViewModel .signUp() + try await signViewModel .signUp(email: email, password: password) let db = Firestore.firestore() guard let userID = Auth.auth().currentUser?.uid else { return } - try await db.collection("USER").document(userID).setData(["email": signViewModel.email, "name": name]) + try await db.collection("USER").document(userID).setData(["email": email, "name": name, "photoUrl": ""]) try await db.collection("BookMark").document(userID).setData(["List": FieldValue.arrayUnion([["word":"", "description" : ""]])]) try await db.collection("Rank").document(userID).setData(["List": FieldValue.arrayUnion([["name":name, "score": 0]])]) - isSignUpActive.toggle() - isDone.toggle() + isAlertActive.toggle() + isDoneActive.toggle() + messageString = contentViewModel.isKR ? "Sign up is correct" : "회원가입이 완료되었습니다." return } catch { - isValidEmails = isValidEmail(email: signViewModel.email) - isValidPasswords = isValidPassword(pwd: signViewModel.password) - emailCheck = emailCheck(email: signViewModel.email) + isValidEmails = isValidEmail(email: email) + isValidPasswords = isValidPassword(pwd: password) + emailCheck = emailCheck(email: email) if !isValidEmails || !isValidPasswords || !emailCheck { - isError = true if !isValidEmails && isValidPasswords { messageString = contentViewModel.isKR ? "Please check the email form" : "이메일 양식을 확인해주세요" } @@ -99,19 +97,28 @@ struct SignUpView: View { else if !emailCheck { messageString = contentViewModel.isKR ? "This Email is already sign up" : "이미 가입된 이메일 입니다" } + isAlertActive.toggle() + isError.toggle() } } } } label: { Text(contentViewModel.isKR ? "Sign up" : "회원가입") .frame(width: 100, height: 35) - + } - .alert(isPresented: $isError) { - Alert(title: Text(contentViewModel.isKR ? "Error" : "경고"), message: Text(messageString), dismissButton: .default(Text(contentViewModel.isKR ? "Done" : "확인"))) - } - .alert(isPresented: $isDone) { - Alert(title: Text(contentViewModel.isKR ? "Notification" : "알림"), message: Text("회원가입이 완료되었습니다"), dismissButton: .default(Text(contentViewModel.isKR ? "Done" : "확인"))) + .alert(isPresented: $isAlertActive) { + Alert(title: Text(contentViewModel.isKR ? "Notification" : "알림"), message: Text(messageString), dismissButton: .default(Text(contentViewModel.isKR ? "Done" : "확인"), action: { + if isDoneActive { + isSignUpActive.toggle() + isAlertActive = false + isDoneActive = false + } + else { + isAlertActive = false + isError = false + } + })) } .disabled(name.isEmpty) .padding(.horizontal, 100) @@ -122,7 +129,7 @@ struct SignUpView: View { .navigationTitle(contentViewModel.isKR ? "Sign up" : "회원가입") .navigationBarTitleDisplayMode(.inline) - + } } } @@ -144,7 +151,6 @@ extension SignUpView { //이메일 중복가입 여부확인 func emailCheck(email: String) -> Bool { var result = false - let userDB = Firestore.firestore().collection("USER") let query = userDB.whereField("email", isEqualTo: email) query.getDocuments() { (qs, err) in diff --git a/EngAttack/Sign/SignViewModel.swift b/EngAttack/Sign/SignViewModel.swift index c26a154..e8d774b 100644 --- a/EngAttack/Sign/SignViewModel.swift +++ b/EngAttack/Sign/SignViewModel.swift @@ -20,32 +20,80 @@ import FirebaseAuth @MainActor final class SignViewModel : ObservableObject { @AppStorage("uid") var uid = "" - @AppStorage("name") var name = "" - @Published var email = "" + @AppStorage("name") var name = "" + @AppStorage("email") var email = "" + @Published var emails = "" @Published var password = "" @Published var Signstate :SignInState = .signedOut @Published var currentUser: Firebase.User? + @Published var authUser: AuthDataResultModel? = nil - init() { currentUser = Auth.auth().currentUser } - func signIn() async throws { + + func loadName() async throws { + guard let userID = Auth.auth().currentUser?.uid else { return } + let db = Firestore.firestore() + try await db.collection("USER").document(userID).getDocument { (doc, error) in + guard error == nil else { + print("error", error ?? "") + return + } + if let doc = doc, doc.exists { + let data = doc.data() + if let data = data { + for i in data { + let email = data["email"] as? String ?? "" + self.name = data["name"] as? String ?? "" + let photoUrl = data["photoUrl"] as? String ?? "" + if self.name != "" { + return + } + } + } + else { + return + } + } + if self.name != "" { + return + } + } + + + } + + func signIn(email: String, password: String) async throws { try await AuthenticationManager.shared.signInUser(email: email, password: password) print("로그인 완료") } - func signUp() async throws { + func signUp(email: String, password: String) async throws { try await AuthenticationManager.shared.createUser(email: email, password: password) - + print("회원가입 완료") + } + + func loadAuthUser() { + self.authUser = try? AuthenticationManager.shared.getAuthenticatedUser() } func signOut() throws { try AuthenticationManager.shared.signOut() currentUser = nil + print("로그아웃 완료") + } + + func updatePassword(password :String) async throws { + try await AuthenticationManager.shared.updatePassword(passwords: password) + print("비밀변경완료") + } + + func deleteUser() async throws { + try await AuthenticationManager.shared.deleteUser() + print("회원탈퇴완료") } - }