From 3f522ffa8c39953bfba6821c2a4a1f4924e98e94 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Thu, 31 Aug 2023 11:56:07 +0900 Subject: [PATCH 01/30] =?UTF-8?q?refactor=20#87:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B7=B0=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DSKit/Sources/Lottie/KeymeLottie.swift | 2 +- Projects/Features/Sources/Root/RootView.swift | 100 ++++++++++-------- .../Features/Sources/SignIn/SignInView.swift | 32 +++--- 3 files changed, 78 insertions(+), 56 deletions(-) diff --git a/Projects/DSKit/Sources/Lottie/KeymeLottie.swift b/Projects/DSKit/Sources/Lottie/KeymeLottie.swift index 24c3660e..169f09a7 100644 --- a/Projects/DSKit/Sources/Lottie/KeymeLottie.swift +++ b/Projects/DSKit/Sources/Lottie/KeymeLottie.swift @@ -27,7 +27,7 @@ public struct KeymeLottieView: UIViewRepresentable { lottieView.translatesAutoresizingMaskIntoConstraints = false lottieView.loopMode = loopMode - lottieView.contentMode = .scaleToFill + lottieView.contentMode = .scaleAspectFill lottieView.play(completion: { finished in if finished { completion?() diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index 56d26c43..13c4f981 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -7,7 +7,9 @@ // import SwiftUI +import Core import ComposableArchitecture +import DSKit public struct RootView: View { private let store: StoreOf @@ -22,51 +24,63 @@ public struct RootView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - if viewStore.logInStatus == .notDetermined { - // 여기 걸리면 에러임. 조심하셈. - EmptyView() - } else if viewStore.logInStatus == .loggedOut { - // 회원가입을 하지 않았거나 로그인을 하지 않은 유저 - let loginStore = store.scope( - state: \.$logInStatus, - action: RootFeature.Action.login) - - IfLetStore(loginStore) { store in - SignInView(store: store) - } - } else if viewStore.registrationState?.status == .notDetermined { - // 개인정보 등록 상태를 로딩 중 - ProgressView() - } else if viewStore.registrationState?.status == .needsRegister { - // 개인정보 등록 - let registrationStore = store.scope( - state: \.$registrationState, - action: RootFeature.Action.registration) - - IfLetStore(registrationStore) { store in - RegistrationView(store: store) - } - } else if viewStore.onboardingStatus?.status == .notDetermined { - // 온보딩 상태를 로딩 중 - ProgressView() - } else if viewStore.onboardingStatus?.status == .needsOnboarding { - // 가입했지만 온보딩을 하지 않고 종료했던 유저 - let onboardingStore = store.scope( - state: \.$onboardingStatus, - action: RootFeature.Action.onboarding) - - IfLetStore(onboardingStore) { store in - OnboardingView(store: store) - } - } else { - // 가입했고 온보딩을 진행한 유저 - let mainPageStore = store.scope(state: \.$mainPageState, action: RootFeature.Action.mainPage) + ZStack { + // 애니메이션 부웅.. 부웅.. + KeymeLottieView(asset: .background, loopMode: .autoReverse) + .ignoresSafeArea() - IfLetStore(mainPageStore) { store in - KeymeMainView(store: store) + if viewStore.logInStatus == .loggedIn { + BackgroundBlurringView(style: .dark) + .ignoresSafeArea() .transition(.opacity) - } else: { - Text("에러") + } + + if viewStore.logInStatus == .notDetermined { + // 여기 걸리면 에러임. 조심하셈. + EmptyView() + } else if viewStore.logInStatus == .loggedOut { + // 회원가입을 하지 않았거나 로그인을 하지 않은 유저 + let loginStore = store.scope( + state: \.$logInStatus, + action: RootFeature.Action.login) + + IfLetStore(loginStore) { store in + SignInView(store: store) + } + } else if viewStore.registrationState?.status == .notDetermined { + // 개인정보 등록 상태를 로딩 중 + ProgressView() + } else if viewStore.registrationState?.status == .needsRegister { + // 개인정보 등록 + let registrationStore = store.scope( + state: \.$registrationState, + action: RootFeature.Action.registration) + + IfLetStore(registrationStore) { store in + RegistrationView(store: store) + } + } else if viewStore.onboardingStatus?.status == .notDetermined { + // 온보딩 상태를 로딩 중 + ProgressView() + } else if viewStore.onboardingStatus?.status == .needsOnboarding { + // 가입했지만 온보딩을 하지 않고 종료했던 유저 + let onboardingStore = store.scope( + state: \.$onboardingStatus, + action: RootFeature.Action.onboarding) + + IfLetStore(onboardingStore) { store in + OnboardingView(store: store) + } + } else { + // 가입했고 온보딩을 진행한 유저 + let mainPageStore = store.scope(state: \.$mainPageState, action: RootFeature.Action.mainPage) + + IfLetStore(mainPageStore) { store in + KeymeMainView(store: store) + .transition(.opacity) + } else: { + Text("에러") + } } } } diff --git a/Projects/Features/Sources/SignIn/SignInView.swift b/Projects/Features/Sources/SignIn/SignInView.swift index e18a953e..96574667 100644 --- a/Projects/Features/Sources/SignIn/SignInView.swift +++ b/Projects/Features/Sources/SignIn/SignInView.swift @@ -10,6 +10,7 @@ import AuthenticationServices import ComposableArchitecture +import DSKit import SwiftUI import Network @@ -21,16 +22,27 @@ public struct SignInView: View { } public var body: some View { - VStack(alignment: .center, spacing: 0) { - Spacer() + ZStack(alignment: .center) { + Text.keyme("KEYME", font: .checkResult) + .foregroundColor(.white) + .offset(y: -39) - KakaoLoginButton(store: store) - - AppleLoginButton(store: store) - - GuideMessageView() + VStack(alignment: .center, spacing: 30) { + Spacer() + + VStack(spacing: 16) { + KakaoLoginButton(store: store) + .frame(height: 48) + + AppleLoginButton(store: store) + .frame(height: 48) + } + + GuideMessageView() + } + .padding(.horizontal, 32) + .padding(.bottom, 56) } - .padding() } // 카카오 로그인 버튼 @@ -45,7 +57,6 @@ public struct SignInView: View { .resizable() .scaledToFill() } - .frame(width: 312, height: 48) .cornerRadius(6) } } @@ -68,9 +79,7 @@ public struct SignInView: View { } }) .signInWithAppleButtonStyle(.white) - .frame(width: 312, height: 48) .cornerRadius(6) - .padding(.vertical) } } @@ -99,7 +108,6 @@ public struct SignInView: View { } } .font(.system(size: 11)) - .frame(width: 265, height: 36) } } } From 54d9daef74bd18576e61821eccaf149b12c29d45 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Thu, 31 Aug 2023 19:03:16 +0900 Subject: [PATCH 02/30] =?UTF-8?q?refactor=20#88:=20=EA=B0=9C=EC=9D=B8?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=93=B1=EB=A1=9D=20=EB=B7=B0=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UI/TextField/TextFieldPlaceholder.swift | 22 ++++ .../Registration/RegistrationView.swift | 122 ++++++++++++------ Projects/Features/Sources/Root/RootView.swift | 2 +- 3 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 Projects/Core/Sources/Utility/UI/TextField/TextFieldPlaceholder.swift diff --git a/Projects/Core/Sources/Utility/UI/TextField/TextFieldPlaceholder.swift b/Projects/Core/Sources/Utility/UI/TextField/TextFieldPlaceholder.swift new file mode 100644 index 00000000..ab1e87bc --- /dev/null +++ b/Projects/Core/Sources/Utility/UI/TextField/TextFieldPlaceholder.swift @@ -0,0 +1,22 @@ +// +// TextFieldPlaceholder.swift +// Core +// +// Created by 이영빈 on 2023/08/31. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import SwiftUI + +public extension View { + func placeholder( + when shouldShow: Bool, + alignment: Alignment = .leading, + @ViewBuilder placeholder: () -> Content + ) -> some View { + ZStack(alignment: alignment) { + placeholder().opacity(shouldShow ? 1 : 0) + self + } + } +} diff --git a/Projects/Features/Sources/Registration/RegistrationView.swift b/Projects/Features/Sources/Registration/RegistrationView.swift index 377b654c..79c40520 100644 --- a/Projects/Features/Sources/Registration/RegistrationView.swift +++ b/Projects/Features/Sources/Registration/RegistrationView.swift @@ -7,6 +7,7 @@ // import Core +import DSKit import SwiftUI import PhotosUI @@ -22,6 +23,7 @@ public struct RegistrationView: View { } // 닉네임 관련 프로퍼티 + @FocusState private var isTextFieldFocused: Bool @State private var nickname = "" // 사용자가 새롭게 입력한 닉네임 @State private var beforeNickname = "" // 기존에 입력했던 닉네임 @State private var isShake = false // 최대 글자 수를 넘긴 경우 좌, 우로 떨리는 애니메이션 @@ -32,7 +34,12 @@ public struct RegistrationView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - VStack(spacing: 12) { + VStack(spacing: 0) { + Text.keyme("회원가입", font: .body3Semibold) + .foregroundColor(.white) + .padding(.top, 16) + .padding(.bottom, 24) + // 프로필 이미지를 등록하는 Circle PhotosPicker(selection: $selectedImage, matching: .images, photoLibrary: .shared()) { profileImage(imageData: selectedImageData) @@ -49,57 +56,68 @@ public struct RegistrationView: View { } } - // 닉네임 관련 안내메세지 + Spacer().frame(height: 59) + HStack(alignment: .center, spacing: 4) { - Text("닉네임") - .font(.system(size: 14)) + Text.keyme("닉네임", font: .body3Regular) + .foregroundColor(.white) - Text("(\(nickname.count)/6)") - .font(.system(size: 12)) - .foregroundColor(.gray) + Text.keyme("(\(nickname.count)/6)", font: .caption1) + .foregroundColor(DSKitAsset.Color.keymeMediumgray.swiftUIColor) Spacer() - Text("2~6자리 한글, 영어, 숫자") - .font(.system(size: 12)) + Text.keyme("2~6자리 한글, 영어, 숫자", font: .caption1) + .foregroundColor(.white) } - .padding(.horizontal, 2) + + Spacer().frame(height: 12) // 닉네임을 입력하는 TextField - TextField("닉네임을 입력해주세요.", text: $nickname) - .font(.system(size: 16)) + TextField("Nickname", text: $nickname) + .focused($isTextFieldFocused) + .placeholder(when: nickname.isEmpty, placeholder: { + Text.keyme("닉네임을 입력해주세요.", font: .body3Regular) + .foregroundColor(.white.opacity(0.4)) + }) + .foregroundColor(.white) + .padding(.horizontal, 10) .frame(height: 50) - .padding(.horizontal) .overlay( RoundedRectangle(cornerRadius: 4) - .stroke(.gray, lineWidth: 1) + .stroke(.white.opacity(0.3), lineWidth: 1) ) + .background(.black.opacity(0.4)) .modifier(Shake(isShake: $isShake)) - if !nickname.isEmpty, let isValid = viewStore.isNicknameAvailable { + if + !nickname.isEmpty, + let isValid = viewStore.state.isNicknameAvailable + { ValidateNicknameView(isValid: isValid) + .padding(.top, 12) } + Spacer(minLength: 50) + // 닉네임 관련 안내메세지 Rectangle() .frame(height: 80) - .foregroundColor(.gray) + .foregroundColor( + DSKitAsset.Color.keymeBlack.swiftUIColor.opacity(0.8)) .cornerRadius(8) .overlay( - Text("친구들이 원할하게 문제를 풀 수 있도록, 나를 가장 잘 나타내는 닉네임으로 설정해주세요. \(viewStore.canRegister.description)") - .font(.system(size: 14)) - .fontWeight(.semibold) + Text.keyme("친구들이 원활하게 문제를 풀 수 있도록, 나를 가장 잘 나타내는 닉네임으로 설정해주세요.", font: .body4) + .lineSpacing(10) .foregroundColor(.white) - .padding(.horizontal, 8) ) - - Spacer() - + .padding(.vertical, 24) + // 다음 페이지로 넘어가기 위한 Button Button(action: { viewStore.send( .finishRegister( - nickname: viewStore.state.nicknameTextFieldString, + nickname: viewStore.nicknameTextFieldString, thumbnailURL: viewStore.thumbnailURL, originalImageURL: viewStore.originalImageURL)) }) { @@ -115,11 +133,14 @@ public struct RegistrationView: View { } } .foregroundColor(.white) - .background(viewStore.canRegister ? .black : .gray) + .background(viewStore.state.canRegister ? .black : .white.opacity(0.3)) .cornerRadius(16) - .disabled(viewStore.canRegister ? false : true) + .padding(.bottom, 20) + .disabled(viewStore.state.canRegister ? false : true) + } + .onTapGesture { + isTextFieldFocused = false } - .modifier(DismissKeyboardOnTap()) .padding(.horizontal, 16) .onChange(of: nickname) { newValue in guard 1 <= newValue.count, newValue.count <= 6 else { @@ -136,6 +157,8 @@ public struct RegistrationView: View { viewStore.send(.debouncedNicknameUpdate(text: newValue)) } + .fullFrame() + .ignoresSafeArea(.keyboard, edges: .bottom) } } } @@ -143,7 +166,15 @@ public struct RegistrationView: View { // 닉네임에 대한 검증 여부를 보여주는 뷰 extension RegistrationView { func profileImage(imageData: Data?) -> some View { - ZStack(alignment: .bottomTrailing) { + let outercircleSize = 160.0 + let iconSize = 33.3 + + return ZStack(alignment: .center) { + Circle() + .foregroundColor(.white.opacity(0.15)) + .overlay(Circle().stroke(.white.opacity(0.30), lineWidth: 1)) + .frame(width: outercircleSize, height: outercircleSize) + Group { if let selectedImageData = imageData, @@ -152,25 +183,22 @@ extension RegistrationView { Image(uiImage: profileImage) .resizable() .scaledToFill() + .frame(width: outercircleSize - 20, height: outercircleSize - 20) } else { - Circle() - .foregroundColor(.gray) - .overlay(Circle().stroke(.white, lineWidth: 1)) + ZStack { + Circle() + .foregroundColor(.black.opacity(0.8)) + .frame(width: outercircleSize - 20, height: outercircleSize - 20) + + Image(systemName: "photo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: iconSize, height: iconSize) + .foregroundColor(.white) + } } } - .frame(width: 160, height: 160) .clipShape(Circle()) - - ZStack { - Circle() - .foregroundColor(.black) - .frame(width: 50, height: 50) - Image(systemName: "photo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 33.3, height: 33.3) - .foregroundColor(.white) - } } } @@ -192,3 +220,11 @@ extension RegistrationView { } } } + +struct RegistrationView_preview: PreviewProvider { + static var previews: some View { + RegistrationView(store: Store(initialState: RegistrationFeature.State(), reducer: { + RegistrationFeature() + })) + } +} diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index 13c4f981..c6bd6821 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -43,7 +43,7 @@ public struct RootView: View { let loginStore = store.scope( state: \.$logInStatus, action: RootFeature.Action.login) - + IfLetStore(loginStore) { store in SignInView(store: store) } From 2ce9cb2b91730a8df72e9681e44b0a82dfed682b Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Fri, 1 Sep 2023 23:39:31 +0900 Subject: [PATCH 03/30] =?UTF-8?q?refactor=20#87:=20=EB=A3=A8=ED=8A=B8?= =?UTF-8?q?=EB=B7=B0=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit . --- .../ProjectDescriptionHelpers/InfoPlist.swift | 10 +- .../Registration/RegisterFeature.swift | 7 +- .../Registration/RegistrationView.swift | 2 +- .../Features/Sources/Root/RootFeature.swift | 251 ++++++++---------- Projects/Features/Sources/Root/RootView.swift | 44 +-- .../Network/Sources/DTO/MemberUpdateDTO.swift | 11 +- 6 files changed, 151 insertions(+), 174 deletions(-) diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift index 49b02f63..cc915eca 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -20,7 +20,15 @@ public extension Project { ] ], "API_BASE_URL": "$(API_BASE_URL)", - "UIUserInterfaceStyle": "Light" + "UIUserInterfaceStyle": "Light", + "NSAppTransportSecurity": [ + "NSExceptionDomains": [ + "api.keyme.space": [ + "NSIncludesSubdomains": true, + "NSExceptionMinimumTLSVersion": "TLSv1.2", + ], + ] + ] ] static let baseUrlInfoPlist: [String: InfoPlist.Value] = [ diff --git a/Projects/Features/Sources/Registration/RegisterFeature.swift b/Projects/Features/Sources/Registration/RegisterFeature.swift index 9f34c452..5005361d 100644 --- a/Projects/Features/Sources/Registration/RegisterFeature.swift +++ b/Projects/Features/Sources/Registration/RegisterFeature.swift @@ -47,7 +47,7 @@ public struct RegistrationFeature: Reducer { case registerProfileImageResponse(thumbnailURL: URL, originalImageURL: URL) case finishRegister(nickname: String, thumbnailURL: URL?, originalImageURL: URL?) - case finishRegisterResponse(id: Int, friendCode: String) + case finishRegisterResponse(MemberUpdateDTO) } public var body: some Reducer { @@ -112,10 +112,7 @@ public struct RegistrationFeature: Reducer { profileThumbnail: originalImageURL?.absoluteString)), object: MemberUpdateDTO.self) - await send( - .finishRegisterResponse( - id: result.data.id, - friendCode: result.data.friendCode ?? "")) // TODO: 나중에 non-null 값 필요 + await send(.finishRegisterResponse(result)) } case .finishRegisterResponse: diff --git a/Projects/Features/Sources/Registration/RegistrationView.swift b/Projects/Features/Sources/Registration/RegistrationView.swift index 79c40520..0e174bf9 100644 --- a/Projects/Features/Sources/Registration/RegistrationView.swift +++ b/Projects/Features/Sources/Registration/RegistrationView.swift @@ -187,7 +187,7 @@ extension RegistrationView { } else { ZStack { Circle() - .foregroundColor(.black.opacity(0.8)) + .foregroundColor(DSKitAsset.Color.keymeBlack.swiftUIColor.opacity(0.8)) .frame(width: outercircleSize - 20, height: outercircleSize - 20) Image(systemName: "photo") diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index ea4e001d..8c2b064d 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -23,161 +23,58 @@ public struct RootFeature: Reducer { public struct State: Equatable { @PresentationState public var logInStatus: SignInFeature.State? @PresentationState public var registrationState: RegistrationFeature.State? - @PresentationState public var onboardingStatus: OnboardingFeature.State? + @PresentationState public var onboardingState: OnboardingFeature.State? @PresentationState public var mainPageState: MainPageFeature.State? - public init( - isLoggedIn: Bool? = nil, - doneRegistration: Bool? = nil, - doneOnboarding: Bool? = nil - ) { - registrationState = .init() - onboardingStatus = .init() - - if let isLoggedIn { - logInStatus = isLoggedIn ? .loggedIn : .loggedOut - } else { - logInStatus = .notDetermined - } - - if let doneRegistration { - registrationState?.status = doneRegistration ? .complete : .needsRegister - } else { - registrationState?.status = .notDetermined - } - - if let doneOnboarding { - onboardingStatus?.status = doneOnboarding ? .completed : .needsOnboarding - } else { - onboardingStatus?.status = .notDetermined - } + var userStatus: UserStatus = .notDetermined + + public enum UserStatus: Equatable { + case notDetermined + case needSignIn + case needRegistration + case needOnboarding + case canUseApp(userId: Int, nickname: String) } } public enum Action { + public enum View { + case checkUserStatus + } + case view(View) + case login(PresentationAction) case registration(PresentationAction) case onboarding(PresentationAction) case mainPage(PresentationAction) - case onboardingChecked(TaskResult) - - case checkUserStatus - case checkLoginStatus - case checkRegistrationStatus - case checkOnboardingStatus - - case updateMemberInformation - case startMainPage(userId: Int, nickname: String) + case updateState(State.UserStatus) + case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?) } public var body: some ReducerOf { Reduce { state, action in switch action { - case .login(.presented(.signInWithAppleResponse(let response))): - switch response { - case .success(let body): - let token = body.data.token.accessToken - userStorage.accessToken = token - network.registerAuthorizationToken(token) - - if body.data.nickname == nil { - state.registrationState?.status = .needsRegister - } else { - state.registrationState?.status = .complete - } - return .none - - case .failure: - return .none - } - - case .login(.presented(.signInWithKakaoResponse(let response))): - switch response { - case .success(let body): - let token = body.data.token.accessToken - userStorage.accessToken = token - network.registerAuthorizationToken(token) - - if body.data.nickname == nil { - state.registrationState?.status = .needsRegister - } else { - state.registrationState?.status = .complete - } - return .none - - case .failure: - return .none - } - - case .checkUserStatus: + case .view(.checkUserStatus): let accessToken = userStorage.accessToken - if accessToken == nil { - state.logInStatus = .loggedOut - - return .none - } else { - state.logInStatus = .loggedIn + if accessToken == nil { // 로그 아웃 상태 + return .send(.updateState(.needSignIn)) + } else { // 로그인 상태 network.registerAuthorizationToken(accessToken) - - return .run { send in - await send(.updateMemberInformation) - await send(.checkRegistrationStatus) - await send(.checkOnboardingStatus) - } - } - - case .checkLoginStatus: - let accessToken = userStorage.accessToken - if accessToken == nil { - state.logInStatus = .loggedOut - } else { - state.logInStatus = .loggedIn - network.registerAuthorizationToken(accessToken) - } - - return .none - - case .registration(.presented(.finishRegisterResponse)): - // Do nothing currently - return .none - - case .checkRegistrationStatus: - let nickname: String? = userStorage.nickname - - if nickname == nil { - state.registrationState?.status = .needsRegister - } else { - state.registrationState?.status = .complete + return .send(.updateMemberInformation(withMemberData: nil)) } - - return .none - - case .checkOnboardingStatus: - return .run(priority: .userInitiated) { send in - await send(.onboardingChecked( - TaskResult { - // TODO: API 갈아끼우기 -// try await Task.sleep(until: .now + .seconds(0.1), clock: .continuous) - return false - } - )) - } - - case .onboardingChecked(.success(let result)): - switch result { - case true: - state.onboardingStatus?.status = .completed - case false: - state.onboardingStatus?.status = .needsOnboarding - } - return .none - - case .updateMemberInformation: + + case .updateMemberInformation(let receviedMemberData): return .run(priority: .userInitiated) { send in - let memberInformation = try await network.request( - .member(.fetch), - object: MemberUpdateDTO.self).data + let memberInformation: MemberUpdateDTO.MemberData + if let receviedMemberData { + memberInformation = receviedMemberData + } else { + memberInformation = try await network.request( + .member(.fetch), + object: MemberUpdateDTO.self + ).data + } userStorage.userId = memberInformation.id userStorage.nickname = memberInformation.nickname @@ -194,25 +91,83 @@ public struct RootFeature: Reducer { userStorage.profileThumbnailURL = profileThumbnailURL } + if memberInformation.nickname == nil { + await send(.updateState(.needRegistration)) + } else if memberInformation.isOnboardingClear == false { + await send(.updateState(.needOnboarding)) + } else { + await send(.updateState(.canUseApp(userId: memberInformation.id, nickname: memberInformation.nickname))) + } + Task.detached(priority: .low) { let notificationDelegate = UserNotificationCenterDelegateManager() + guard let token = await notificationDelegate.waitForToken() else { - print("ERROR TOEKN PUSH") + print("푸시토큰 등록 중 에러 발생") return } _ = try await network.request(.registerPushToken(.register(token))) } - - await send(.startMainPage( - userId: memberInformation.id, - nickname: memberInformation.nickname)) } - case .startMainPage(let userId, let nickname): - state.mainPageState = MainPageFeature.State(userId: userId, nickname: nickname) + case .updateState(let status): + state.userStatus = status + + switch status { + case .notDetermined: + break + case .needSignIn: + state.logInStatus = .loggedIn + case .needRegistration: + state.registrationState = .init() + case .needOnboarding: + state.onboardingState = .init() + case let .canUseApp(userId, nickname): + state.mainPageState = .init(userId: userId, nickname: nickname) + } + return .none + // MARK: - Presentation actions + case .login(.presented(.signInWithAppleResponse(let response))): + switch response { + case .success(let body): + let token = body.data.token.accessToken + userStorage.accessToken = token + network.registerAuthorizationToken(token) + + return .send(.updateMemberInformation(withMemberData: nil)) + + case .failure: + return .none + } + + case .login(.presented(.signInWithKakaoResponse(let response))): + switch response { + case .success(let body): + let token = body.data.token.accessToken + userStorage.accessToken = token + network.registerAuthorizationToken(token) + + return .send(.updateMemberInformation(withMemberData: nil)) + + case .failure: + return .none + } + + case .registration(.presented(.finishRegisterResponse(let response))): + return .send(.updateMemberInformation(withMemberData: response.data)) + + case .onboarding(.presented(.testResult(.closeButtonDidTap))): + guard let userId = userStorage.userId, let nickname = userStorage.nickname else { + // 멤버 정보 수신 재시도 + // TODO: and show alert. 사실 있을 수 없는 케이스긴 함 + return .send(.updateMemberInformation(withMemberData: nil)) + } + + return .send(.updateState(.canUseApp(userId: userId, nickname: nickname))) + default: return .none } @@ -223,7 +178,7 @@ public struct RootFeature: Reducer { .ifLet(\.$registrationState, action: /Action.registration) { RegistrationFeature() } - .ifLet(\.$onboardingStatus, action: /Action.onboarding) { + .ifLet(\.$onboardingState, action: /Action.onboarding) { OnboardingFeature() } .ifLet(\.$mainPageState, action: /Action.mainPage) { @@ -231,3 +186,13 @@ public struct RootFeature: Reducer { } } } + +private extension RootFeature { + func needsRegistration(forNickname nickname: String?) -> Bool { + if nickname == nil { + return true + } else { + return false + } + } +} diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index c6bd6821..0d8a98d0 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -16,41 +16,37 @@ public struct RootView: View { public init() { self.store = Store(initialState: RootFeature.State()) { - RootFeature() + RootFeature()._printChanges() } - store.send(.checkUserStatus) + store.send(.view(.checkUserStatus)) } public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(store, observe: { $0.userStatus }, send: RootFeature.Action.view) { viewStore in ZStack { // 애니메이션 부웅.. 부웅.. KeymeLottieView(asset: .background, loopMode: .autoReverse) .ignoresSafeArea() - if viewStore.logInStatus == .loggedIn { + if viewStore.state != .needSignIn { BackgroundBlurringView(style: .dark) .ignoresSafeArea() - .transition(.opacity) + .transition(.opacity.animation(.easeInOut)) } - if viewStore.logInStatus == .notDetermined { - // 여기 걸리면 에러임. 조심하셈. - EmptyView() - } else if viewStore.logInStatus == .loggedOut { + switch viewStore.state { + case .needSignIn: // 회원가입을 하지 않았거나 로그인을 하지 않은 유저 let loginStore = store.scope( state: \.$logInStatus, action: RootFeature.Action.login) - + IfLetStore(loginStore) { store in SignInView(store: store) } - } else if viewStore.registrationState?.status == .notDetermined { - // 개인정보 등록 상태를 로딩 중 - ProgressView() - } else if viewStore.registrationState?.status == .needsRegister { + + case .needRegistration: // 개인정보 등록 let registrationStore = store.scope( state: \.$registrationState, @@ -59,28 +55,32 @@ public struct RootView: View { IfLetStore(registrationStore) { store in RegistrationView(store: store) } - } else if viewStore.onboardingStatus?.status == .notDetermined { - // 온보딩 상태를 로딩 중 - ProgressView() - } else if viewStore.onboardingStatus?.status == .needsOnboarding { + + case .needOnboarding: // 가입했지만 온보딩을 하지 않고 종료했던 유저 let onboardingStore = store.scope( - state: \.$onboardingStatus, + state: \.$onboardingState, action: RootFeature.Action.onboarding) IfLetStore(onboardingStore) { store in OnboardingView(store: store) } - } else { + + case .canUseApp: // 가입했고 온보딩을 진행한 유저 - let mainPageStore = store.scope(state: \.$mainPageState, action: RootFeature.Action.mainPage) + let mainPageStore = store.scope( + state: \.$mainPageState, + action: RootFeature.Action.mainPage) IfLetStore(mainPageStore) { store in KeymeMainView(store: store) - .transition(.opacity) + .transition(.opacity.animation(.easeInOut)) } else: { Text("에러") } + + case .notDetermined: + EmptyView() } } } diff --git a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift index cac54c30..a9486163 100644 --- a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift +++ b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift @@ -8,16 +8,23 @@ import Foundation -public struct MemberUpdateDTO: Codable { +public struct MemberUpdateDTO: Codable, Equatable { let code: Int public let data: MemberData let message: String - public struct MemberData: Codable { + public struct MemberData: Codable, Equatable { public let friendCode: String? public let id: Int + public let isOnboardingClear: Bool public let nickname: String public let profileImage: String public let profileThumbnail: String } } + +extension MemberUpdateDTO { + public static func == (lhs: MemberUpdateDTO, rhs: MemberUpdateDTO) -> Bool { + lhs.data.id == rhs.data.id + } +} From 873edaf6520d13b432c93f2ad75e14b414212714 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 09:39:52 +0900 Subject: [PATCH 04/30] =?UTF-8?q?refactor=20#87:=20=EB=A3=A8=ED=8A=B8?= =?UTF-8?q?=EB=B7=B0=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=93=B1=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/Root/RootFeature+.swift | 30 +------------------ .../Features/Sources/Root/RootFeature.swift | 17 +++++------ Projects/Keyme/Sources/KeymeApp.swift | 12 ++++++++ .../Network/Sources/DTO/MemberUpdateDTO.swift | 4 +-- 4 files changed, 23 insertions(+), 40 deletions(-) diff --git a/Projects/Features/Sources/Root/RootFeature+.swift b/Projects/Features/Sources/Root/RootFeature+.swift index 45f9ca0e..9ce98bd2 100644 --- a/Projects/Features/Sources/Root/RootFeature+.swift +++ b/Projects/Features/Sources/Root/RootFeature+.swift @@ -35,7 +35,7 @@ extension RootFeature { // Wait for the token to be received DispatchQueue.global().async { - _ = self.tokenSemaphore.wait(timeout: .now() + 20) + _ = self.tokenSemaphore.wait(timeout: .now() + 10) if let token = self.fcmToken { continuation.resume(returning: token) } else { @@ -47,20 +47,6 @@ extension RootFeature { private func startRegister() { Messaging.messaging().delegate = self - - UNUserNotificationCenter.current().delegate = self - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in - guard granted else { return } - - // 푸시토큰 애플 서버에 등록하기 - UNUserNotificationCenter.current().getNotificationSettings { settings in - guard settings.authorizationStatus == .authorized else { return } - - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } } } } @@ -75,17 +61,3 @@ extension RootFeature.UserNotificationCenterDelegateManager: MessagingDelegate { tokenSemaphore.signal() } } - -extension RootFeature.UserNotificationCenterDelegateManager: UNUserNotificationCenterDelegate { - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } - let token = tokenParts.joined() - Messaging.messaging().setAPNSToken(deviceToken, type: .unknown) - - print("firebase Device Token: \(token)") - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("firebase Failed to register for remote notifications: \(error)") - } -} diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 8c2b064d..8a0e1389 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -78,10 +78,7 @@ public struct RootFeature: Reducer { userStorage.userId = memberInformation.id userStorage.nickname = memberInformation.nickname - - if let friendCode = memberInformation.friendCode { - userStorage.friendCode = friendCode - } + userStorage.friendCode = memberInformation.friendCode if let profileImageURL = URL(string: memberInformation.profileImage) { userStorage.profileImageURL = profileImageURL @@ -91,12 +88,14 @@ public struct RootFeature: Reducer { userStorage.profileThumbnailURL = profileThumbnailURL } - if memberInformation.nickname == nil { - await send(.updateState(.needRegistration)) - } else if memberInformation.isOnboardingClear == false { - await send(.updateState(.needOnboarding)) + if let userId = memberInformation.id, let nickname = memberInformation.nickname { + if memberInformation.isOnboardingClear == false { + await send(.updateState(.needOnboarding)) + } else { + await send(.updateState(.canUseApp(userId: userId, nickname: nickname))) + } } else { - await send(.updateState(.canUseApp(userId: memberInformation.id, nickname: memberInformation.nickname))) + await send(.updateState(.needRegistration)) } Task.detached(priority: .low) { diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index c58d92f7..bf26f2cf 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -55,4 +55,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } return true } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } + let token = tokenParts.joined() + Messaging.messaging().setAPNSToken(deviceToken, type: .unknown) + + print("firebase Device Token: \(token)") + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("firebase Failed to register for remote notifications: \(error)") + } } diff --git a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift index a9486163..1c6a9276 100644 --- a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift +++ b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift @@ -15,9 +15,9 @@ public struct MemberUpdateDTO: Codable, Equatable { public struct MemberData: Codable, Equatable { public let friendCode: String? - public let id: Int + public let id: Int? public let isOnboardingClear: Bool - public let nickname: String + public let nickname: String? public let profileImage: String public let profileThumbnail: String } From 2a623964ae61bb9ce004ade1338689f7e4aeee72 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 12:07:26 +0900 Subject: [PATCH 05/30] =?UTF-8?q?refactor=20#87:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20APi=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/KeymeTestHomeFeature.swift | 8 ++--- .../Sources/Home/KeymeTestHomeView.swift | 6 +++- .../Sources/MainPage/MainPageFeature.swift | 21 ++++++++---- .../Sources/MainPage/MainPageView.swift | 33 +++++++------------ .../Sources/MyPage/MyPageFeature.swift | 24 +++++++------- .../Features/Sources/MyPage/MyPageView.swift | 11 ++++--- .../MyPage/ScoreList/ScoreListFeature.swift | 2 +- 7 files changed, 54 insertions(+), 51 deletions(-) diff --git a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift index 2a80651c..c56da995 100644 --- a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift @@ -10,11 +10,11 @@ import ComposableArchitecture import Domain import Network -struct KeymeTestsHomeFeature: Reducer { +public struct KeymeTestsHomeFeature: Reducer { @Dependency(\.keymeAPIManager) private var network // 테스트를 아직 풀지 않았거나, 풀었거나 2가지 케이스만 존재 - struct State: Equatable { + public struct State: Equatable { @PresentationState var testStartViewState: KeymeTestsStartFeature.State? var view: View @@ -28,7 +28,7 @@ struct KeymeTestsHomeFeature: Reducer { } } - enum Action { + public enum Action { case fetchDailyTests case showTestStartView(testData: KeymeTestsModel) case startTest(PresentationAction) @@ -36,7 +36,7 @@ struct KeymeTestsHomeFeature: Reducer { enum View {} } - var body: some ReducerOf { + public var body: some ReducerOf { Reduce { state, action in switch action { case .fetchDailyTests: diff --git a/Projects/Features/Sources/Home/KeymeTestHomeView.swift b/Projects/Features/Sources/Home/KeymeTestHomeView.swift index a4bae6ab..4ba42556 100644 --- a/Projects/Features/Sources/Home/KeymeTestHomeView.swift +++ b/Projects/Features/Sources/Home/KeymeTestHomeView.swift @@ -15,7 +15,6 @@ struct KeymeTestsHomeView: View { init(store: StoreOf) { self.store = store - store.send(.fetchDailyTests) } var body: some View { @@ -41,6 +40,11 @@ struct KeymeTestsHomeView: View { // 결과 화면 표시도 생각 } + .onAppear { + if viewStore.dailyTestId == nil { + viewStore.send(.fetchDailyTests) + } + } } } } diff --git a/Projects/Features/Sources/MainPage/MainPageFeature.swift b/Projects/Features/Sources/MainPage/MainPageFeature.swift index 80fd4e0b..f832dafe 100644 --- a/Projects/Features/Sources/MainPage/MainPageFeature.swift +++ b/Projects/Features/Sources/MainPage/MainPageFeature.swift @@ -11,23 +11,32 @@ import ComposableArchitecture public struct MainPageFeature: Reducer { public struct State: Equatable { - let userId: Int - let nickname: String + var home: KeymeTestsHomeFeature.State + var myPage: MyPageFeature.State public init(userId: Int, nickname: String) { - self.userId = userId - self.nickname = nickname + self.home = .init(nickname: nickname) + self.myPage = .init(userId: userId, nickname: nickname) } } public enum Action { - case logout - case changeNickname(String) + case home(KeymeTestsHomeFeature.Action) + case myPage(MyPageFeature.Action) } public var body: some Reducer { Reduce { _, _ in return .none } + + Scope(state: \.home, action: /Action.home) { + KeymeTestsHomeFeature() + } + + Scope(state: \.myPage, action: /Action.myPage) { + MyPageFeature() + } + } } diff --git a/Projects/Features/Sources/MainPage/MainPageView.swift b/Projects/Features/Sources/MainPage/MainPageView.swift index e8d2b9c8..5880737f 100644 --- a/Projects/Features/Sources/MainPage/MainPageView.swift +++ b/Projects/Features/Sources/MainPage/MainPageView.swift @@ -25,30 +25,19 @@ struct KeymeMainView: View { } var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(store, observe: { $0 }) { _ in TabView(selection: $selectedTab) { - KeymeTestsHomeView(store: Store( - initialState: KeymeTestsHomeFeature.State( - nickname: viewStore.nickname) - ) { - KeymeTestsHomeFeature() - }) - .tabItem { - homeTabImage - } - .tag(Tab.home) + KeymeTestsHomeView(store: store.scope(state: \.home, action: MainPageFeature.Action.home)) + .tabItem { + homeTabImage + } + .tag(Tab.home) - MyPageView(store: Store( - initialState: MyPageFeature.State( - userId: viewStore.state.userId, - nickname: viewStore.state.nickname) - ) { - MyPageFeature() - }) - .tabItem { - myPageTabImage - } - .tag(Tab.myPage) + MyPageView(store: store.scope(state: \.myPage, action: MainPageFeature.Action.myPage)) + .tabItem { + myPageTabImage + } + .tag(Tab.myPage) } .introspect(.tabView, on: .iOS(.v16, .v17)) { tabViewController in let tabBar = tabViewController.tabBar diff --git a/Projects/Features/Sources/MyPage/MyPageFeature.swift b/Projects/Features/Sources/MyPage/MyPageFeature.swift index 23a3b48c..5dd4b8c6 100644 --- a/Projects/Features/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Features/Sources/MyPage/MyPageFeature.swift @@ -14,17 +14,10 @@ import Foundation import SwiftUI import Network -struct Coordinate { - var x: Double - var y: Double - var r: Double - var color: Color -} - -struct MyPageFeature: Reducer { +public struct MyPageFeature: Reducer { @Dependency(\.keymeAPIManager) private var network - struct State: Equatable { + public struct State: Equatable { var similarCircleDataList: [CircleData] = [] var differentCircleDataList: [CircleData] = [] var view: View @@ -45,14 +38,14 @@ struct MyPageFeature: Reducer { } } - enum Action: Equatable { + public enum Action: Equatable { case saveCircle([CircleData], MatchRate) case showCircle(MyPageSegment) case requestCircle(MatchRate) case view(View) case scoreListAction(ScoreListFeature.Action) - enum View: Equatable { + public enum View: Equatable { case markViewAsShown case circleTapped case circleDismissed @@ -138,9 +131,16 @@ struct MyPageFeature: Reducer { } } -extension MyPageFeature { +public extension MyPageFeature { enum MatchRate { case top5 case low5 } + + struct Coordinate { + var x: Double + var y: Double + var r: Double + var color: Color + } } diff --git a/Projects/Features/Sources/MyPage/MyPageView.swift b/Projects/Features/Sources/MyPage/MyPageView.swift index c630bc45..1f8be83b 100644 --- a/Projects/Features/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/Sources/MyPage/MyPageView.swift @@ -18,11 +18,6 @@ struct MyPageView: View { init(store: StoreOf) { self.store = store - - store.send(.requestCircle(.top5)) - store.send(.requestCircle(.low5)) - - store.send(.view(.selectSegement(.similar))) } public var body: some View { @@ -90,5 +85,11 @@ struct MyPageView: View { } } } + .onAppear { + store.send(.requestCircle(.top5)) + store.send(.requestCircle(.low5)) + + store.send(.view(.selectSegement(.similar))) + } } } diff --git a/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift b/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift index ff3c7dd6..20265277 100644 --- a/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift +++ b/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift @@ -11,7 +11,7 @@ import ComposableArchitecture import Domain import Network -struct ScoreListFeature: Reducer { +public struct ScoreListFeature: Reducer { @Dependency(\.keymeAPIManager) private var network public struct State: Equatable { From 8f2488f909819307f10c08505aa6b0c79f9e7879 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 13:29:31 +0900 Subject: [PATCH 06/30] =?UTF-8?q?refactor=20#87:=20=EB=A3=A8=ED=8A=B8?= =?UTF-8?q?=EB=B7=B0=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/Root/RootFeature.swift | 2 +- Projects/Features/Sources/Root/RootView.swift | 24 +++++++++++++++---- .../Network/Sources/DTO/MemberUpdateDTO.swift | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 8a0e1389..89c3738b 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -89,7 +89,7 @@ public struct RootFeature: Reducer { } if let userId = memberInformation.id, let nickname = memberInformation.nickname { - if memberInformation.isOnboardingClear == false { + if memberInformation.isOnboardingClear != true { await send(.updateState(.needOnboarding)) } else { await send(.updateState(.canUseApp(userId: userId, nickname: nickname))) diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index 0d8a98d0..9e0e79cc 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -32,7 +32,7 @@ public struct RootView: View { if viewStore.state != .needSignIn { BackgroundBlurringView(style: .dark) .ignoresSafeArea() - .transition(.opacity.animation(.easeInOut)) + .transition(.opacity.animation(Animation.customInteractiveSpring())) } switch viewStore.state { @@ -45,7 +45,9 @@ public struct RootView: View { IfLetStore(loginStore) { store in SignInView(store: store) } - + .transition(.opacity.animation(Animation.customInteractiveSpring())) + .zIndex(ViewZIndex.siginIn.rawValue) + case .needRegistration: // 개인정보 등록 let registrationStore = store.scope( @@ -55,7 +57,9 @@ public struct RootView: View { IfLetStore(registrationStore) { store in RegistrationView(store: store) } - + .transition(.opacity.animation(Animation.customInteractiveSpring())) + .zIndex(ViewZIndex.registration.rawValue) + case .needOnboarding: // 가입했지만 온보딩을 하지 않고 종료했던 유저 let onboardingStore = store.scope( @@ -65,6 +69,8 @@ public struct RootView: View { IfLetStore(onboardingStore) { store in OnboardingView(store: store) } + .transition(.opacity.animation(Animation.customInteractiveSpring())) + .zIndex(ViewZIndex.onboarding.rawValue) case .canUseApp: // 가입했고 온보딩을 진행한 유저 @@ -74,10 +80,11 @@ public struct RootView: View { IfLetStore(mainPageStore) { store in KeymeMainView(store: store) - .transition(.opacity.animation(.easeInOut)) } else: { Text("에러") } + .transition(.opacity.animation(Animation.customInteractiveSpring())) + .zIndex(ViewZIndex.main.rawValue) case .notDetermined: EmptyView() @@ -87,6 +94,15 @@ public struct RootView: View { } } +private extension RootView { + enum ViewZIndex: CGFloat { + case siginIn = 4 + case registration = 3 + case onboarding = 2 + case main = 1 + } +} + struct RootView_Previews: PreviewProvider { static var previews: some View { RootView() diff --git a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift index 1c6a9276..140cf8a0 100644 --- a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift +++ b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift @@ -16,7 +16,7 @@ public struct MemberUpdateDTO: Codable, Equatable { public struct MemberData: Codable, Equatable { public let friendCode: String? public let id: Int? - public let isOnboardingClear: Bool + public let isOnboardingClear: Bool? public let nickname: String? public let profileImage: String public let profileThumbnail: String From 63c0c273cebee6ba737325bec9cd832a355cbd67 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 13:47:09 +0900 Subject: [PATCH 07/30] =?UTF-8?q?refactor=20#87:=20=ED=96=85=ED=8B=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Utility/Interact/Haptic/HapticManager.swift | 7 +++++++ Projects/Features/Sources/Onboarding/OnboardingView.swift | 2 +- .../Features/Sources/Registration/RegistrationView.swift | 1 + Projects/Features/Sources/SignIn/SignInView.swift | 3 +++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Projects/Core/Sources/Utility/Interact/Haptic/HapticManager.swift b/Projects/Core/Sources/Utility/Interact/Haptic/HapticManager.swift index b3d65ace..a19e943b 100644 --- a/Projects/Core/Sources/Utility/Interact/Haptic/HapticManager.swift +++ b/Projects/Core/Sources/Utility/Interact/Haptic/HapticManager.swift @@ -54,6 +54,13 @@ public class HapticManager { generator.impactOccurred() } + /// 붕 + public func boong() { + let generator = UIImpactFeedbackGenerator(style: .soft) + generator.prepare() + generator.impactOccurred() + } + /// 패턴 재생(패턴은 추후 협의 후 개발해서 추가) public func playHapticPattern() { do { diff --git a/Projects/Features/Sources/Onboarding/OnboardingView.swift b/Projects/Features/Sources/Onboarding/OnboardingView.swift index 2a9fcf71..8da8784f 100644 --- a/Projects/Features/Sources/Onboarding/OnboardingView.swift +++ b/Projects/Features/Sources/Onboarding/OnboardingView.swift @@ -116,7 +116,7 @@ public struct OnboardingView: View { .foregroundColor(.black) } .onTapGesture { - HapticManager.shared.tok() + HapticManager.shared.boong() viewStore.send(.nextButtonDidTap) } .padding(Padding.insets(leading: 16, trailing: 16)) diff --git a/Projects/Features/Sources/Registration/RegistrationView.swift b/Projects/Features/Sources/Registration/RegistrationView.swift index 0e174bf9..48b3ed57 100644 --- a/Projects/Features/Sources/Registration/RegistrationView.swift +++ b/Projects/Features/Sources/Registration/RegistrationView.swift @@ -115,6 +115,7 @@ public struct RegistrationView: View { // 다음 페이지로 넘어가기 위한 Button Button(action: { + HapticManager.shared.boong() viewStore.send( .finishRegister( nickname: viewStore.nicknameTextFieldString, diff --git a/Projects/Features/Sources/SignIn/SignInView.swift b/Projects/Features/Sources/SignIn/SignInView.swift index 96574667..8953e2be 100644 --- a/Projects/Features/Sources/SignIn/SignInView.swift +++ b/Projects/Features/Sources/SignIn/SignInView.swift @@ -9,6 +9,7 @@ // import AuthenticationServices +import Core import ComposableArchitecture import DSKit import SwiftUI @@ -52,6 +53,7 @@ public struct SignInView: View { var body: some View { Button(action: { store.send(.signInWithKakao) + HapticManager.shared.boong() }) { Image("kakao_login") .resizable() @@ -68,6 +70,7 @@ public struct SignInView: View { var body: some View { SignInWithAppleButton( onRequest: { request in + HapticManager.shared.boong() request.requestedScopes = [.fullName, .email] }, onCompletion: { completion in From 05dad48ad0d45bbbfebbf711ad663c838a472637 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 18:29:10 +0900 Subject: [PATCH 08/30] =?UTF-8?q?fix=20#88:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/Registration/RegisterFeature.swift | 8 +++++++- .../Features/Sources/Registration/RegistrationView.swift | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Projects/Features/Sources/Registration/RegisterFeature.swift b/Projects/Features/Sources/Registration/RegisterFeature.swift index 5005361d..0965b63f 100644 --- a/Projects/Features/Sources/Registration/RegisterFeature.swift +++ b/Projects/Features/Sources/Registration/RegisterFeature.swift @@ -54,13 +54,19 @@ public struct RegistrationFeature: Reducer { Reduce { state, action in switch action { case .debouncedNicknameUpdate(let nicknameString): + guard !nicknameString.isEmpty else { + return .none + } + state.nicknameTextFieldString = nicknameString + state.isNicknameAvailable = nil + return .run { send in try await withTaskCancellation( id: CancelID.debouncedNicknameUpdate, cancelInFlight: true ) { - try await self.clock.sleep(for: .seconds(0.7)) + try await self.clock.sleep(for: .seconds(0.3)) await send(.checkDuplicatedNickname(nicknameString)) } diff --git a/Projects/Features/Sources/Registration/RegistrationView.swift b/Projects/Features/Sources/Registration/RegistrationView.swift index 48b3ed57..2d449645 100644 --- a/Projects/Features/Sources/Registration/RegistrationView.swift +++ b/Projects/Features/Sources/Registration/RegistrationView.swift @@ -87,7 +87,10 @@ public struct RegistrationView: View { RoundedRectangle(cornerRadius: 4) .stroke(.white.opacity(0.3), lineWidth: 1) ) - .background(.black.opacity(0.4)) + .background( + RoundedRectangle(cornerRadius: 4) + .fill(.black.opacity(0.4)) + ) .modifier(Shake(isShake: $isShake)) if @@ -111,8 +114,8 @@ public struct RegistrationView: View { .lineSpacing(10) .foregroundColor(.white) ) - .padding(.vertical, 24) - + .padding(.bottom, 64) + // 다음 페이지로 넘어가기 위한 Button Button(action: { HapticManager.shared.boong() From 7f4977e69b363ba9f8603b6f32c85bb92d8581ac Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 18:57:32 +0900 Subject: [PATCH 09/30] =?UTF-8?q?refactor=20#87:=20=EC=95=BD=EA=B4=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Onboarding/OnboardingView.swift | 2 +- .../Sources/Registration/RegistrationView.swift | 7 ++++--- .../Features/Sources/SignIn/SignInView.swift | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Projects/Features/Sources/Onboarding/OnboardingView.swift b/Projects/Features/Sources/Onboarding/OnboardingView.swift index 8da8784f..5dc5a9aa 100644 --- a/Projects/Features/Sources/Onboarding/OnboardingView.swift +++ b/Projects/Features/Sources/Onboarding/OnboardingView.swift @@ -99,7 +99,7 @@ public struct OnboardingView: View { Color.clear .contentShape(Circle()) .onTapGesture { - HapticManager.shared.tongtong() + HapticManager.shared.boong() viewStore.send(.startButtonDidTap) } } diff --git a/Projects/Features/Sources/Registration/RegistrationView.swift b/Projects/Features/Sources/Registration/RegistrationView.swift index 2d449645..420b4e84 100644 --- a/Projects/Features/Sources/Registration/RegistrationView.swift +++ b/Projects/Features/Sources/Registration/RegistrationView.swift @@ -142,9 +142,6 @@ public struct RegistrationView: View { .padding(.bottom, 20) .disabled(viewStore.state.canRegister ? false : true) } - .onTapGesture { - isTextFieldFocused = false - } .padding(.horizontal, 16) .onChange(of: nickname) { newValue in guard 1 <= newValue.count, newValue.count <= 6 else { @@ -164,6 +161,10 @@ public struct RegistrationView: View { .fullFrame() .ignoresSafeArea(.keyboard, edges: .bottom) } + .contentShape(Rectangle()) + .onTapGesture { + isTextFieldFocused = false + } } } diff --git a/Projects/Features/Sources/SignIn/SignInView.swift b/Projects/Features/Sources/SignIn/SignInView.swift index 8953e2be..9ba1d816 100644 --- a/Projects/Features/Sources/SignIn/SignInView.swift +++ b/Projects/Features/Sources/SignIn/SignInView.swift @@ -87,13 +87,21 @@ public struct SignInView: View { } struct GuideMessageView: View { + let serviceTermURLString = "https://keyme.notion.site/Keyme-b1f3902d8fe04b97be6d8835119887cd?pvs=4" + let privacyTermURLString = "https://keyme.notion.site/Keyme-46bef61be1204fc594a49e85e5913a39?pvs=4" + var body: some View { VStack(spacing: 8) { Text("가입 시, 키미의 다음 사항에 동의하는 것으로 간주합니다.") .foregroundColor(.gray) HStack(spacing: 4) { - Button(action: {}) { + Button(action: { + guard let serviceTermURL = URL(string: serviceTermURLString) else { + return + } + UIApplication.shared.open(serviceTermURL) + }) { Text("서비스 이용약관") .fontWeight(.bold) .foregroundColor(.white) @@ -102,7 +110,12 @@ public struct SignInView: View { Text("및") .foregroundColor(.gray) - Button(action: {}) { + Button(action: { + guard let privacyTermURL = URL(string: privacyTermURLString) else { + return + } + UIApplication.shared.open(privacyTermURL) + }) { Text("개인정보 정책") .fontWeight(.bold) .foregroundColor(.white) From f499971db7a0a1a481c1fe250b030d570d579f5f Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 19:20:52 +0900 Subject: [PATCH 10/30] =?UTF-8?q?refactor=20#87:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 3 ++- Projects/Keyme/Sources/KeymeApp.swift | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift index cc915eca..1c75b7a6 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -28,7 +28,8 @@ public extension Project { "NSExceptionMinimumTLSVersion": "TLSv1.2", ], ] - ] + ], + "LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaolink"] ] static let baseUrlInfoPlist: [String: InfoPlist.Value] = [ diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index bf26f2cf..8c704728 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -24,11 +24,6 @@ struct KeymeApp: App { var body: some Scene { WindowGroup { RootView() - .onOpenURL(perform: { url in - if (AuthApi.isKakaoTalkLoginUrl(url)) { - AuthController.handleOpenUrl(url: url) - } - }) } } } @@ -56,6 +51,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return true } + func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey : Any] = [:] + ) -> Bool { + if (AuthApi.isKakaoTalkLoginUrl(url)) { + return AuthController.handleOpenUrl(url: url) + } + + return false + } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } let token = tokenParts.joined() From b3b93be73053c7506b1614d275d8b43094eec5c4 Mon Sep 17 00:00:00 2001 From: enebin Date: Sun, 3 Sep 2023 07:58:18 +0900 Subject: [PATCH 11/30] =?UTF-8?q?refactor=20#87:=20xcconfig=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 6 +++- Projects/Keyme/Sources/KeymeApp.swift | 30 +++++++------------ .../Sources/Network/API/ShortUrlAPI.swift | 9 ++++-- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift index 1c75b7a6..154fa3a6 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -16,10 +16,14 @@ public extension Project { "CFBundleURLTypes": [ [ "CFBundleTypeRole": "Editor", - "CFBundleURLSchemes": ["keyme"] + "CFBundleURLSchemes": [ + "keyme", + "kakao$(KAKAO_API_KEY)" + ] ] ], "API_BASE_URL": "$(API_BASE_URL)", + "KAKAO_API_KEY": "$(KAKAO_API_KEY)", "UIUserInterfaceStyle": "Light", "NSAppTransportSecurity": [ "NSExceptionDomains": [ diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index 8c704728..fff8c7f6 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -8,22 +8,22 @@ import FirebaseMessaging import Features import Network -import KakaoSDKAuth import KakaoSDKCommon +import KakaoSDKAuth @main struct KeymeApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - let KAKAO_PRIVATE_KEY = "" // 🚨 SECRET 🚨 - - init() { - KakaoSDK.initSDK(appKey: KAKAO_PRIVATE_KEY) - } - var body: some Scene { WindowGroup { RootView() + .onOpenURL { url in + print(url) + if (AuthApi.isKakaoTalkLoginUrl(url)) { + _ = AuthController.handleOpenUrl(url: url) + } + } } } } @@ -33,6 +33,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + if let kakaoAPIKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_API_KEY") as? String { + KakaoSDK.initSDK(appKey: kakaoAPIKey) + } + FirebaseApp.configure() UNUserNotificationCenter.current().delegate = self @@ -51,18 +55,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return true } - func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey : Any] = [:] - ) -> Bool { - if (AuthApi.isKakaoTalkLoginUrl(url)) { - return AuthController.handleOpenUrl(url: url) - } - - return false - } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } let token = tokenParts.joined() diff --git a/Projects/Network/Sources/Network/API/ShortUrlAPI.swift b/Projects/Network/Sources/Network/API/ShortUrlAPI.swift index b42ab6d0..2a30eb9d 100644 --- a/Projects/Network/Sources/Network/API/ShortUrlAPI.swift +++ b/Projects/Network/Sources/Network/API/ShortUrlAPI.swift @@ -44,7 +44,12 @@ extension ShortUrlAPI: TargetType { } public var headers: [String : String]? { - let accessToken = "e9a1ab0011a56327138c36652c2242cdff37ee1b" // TODO: 밖으로 - return ["Authorization": "Bearer \(accessToken)", "Content-Type": "application/json"] + var header = ["Content-Type": "application/json"] + + if let accessToken = Bundle.main.object(forInfoDictionaryKey: "BITLY_API_KEY") as? String { + header["Authorization"] = "Bearer \(accessToken)" + } + + return header } } From e983ace429f1a4ed1653e915f0c117ec67ae7dc1 Mon Sep 17 00:00:00 2001 From: enebin Date: Sat, 2 Sep 2023 19:56:56 +0900 Subject: [PATCH 12/30] =?UTF-8?q?feature=20#89:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=ED=8F=AC=ED=86=A0/?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo_export.imageset/Contents.json | 23 ++++++++++++++++++ .../photo_export.imageset/photo_export.png | Bin 0 -> 1057 bytes .../photo_export.imageset/photo_export@2x.png | Bin 0 -> 2143 bytes .../photo_export.imageset/photo_export@3x.png | Bin 0 -> 3435 bytes .../setting.imageset/Contents.json | 23 ++++++++++++++++++ .../setting.imageset/setting.png | Bin 0 -> 530 bytes .../setting.imageset/setting@2x.png | Bin 0 -> 1043 bytes .../setting.imageset/setting@3x.png | Bin 0 -> 1564 bytes .../Features/Sources/MyPage/MyPageView.swift | 15 ++++++++++++ 9 files changed, 61 insertions(+) create mode 100644 Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/Contents.json create mode 100644 Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export.png create mode 100644 Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export@2x.png create mode 100644 Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export@3x.png create mode 100644 Projects/DSKit/Resources/Image.xcassets/setting.imageset/Contents.json create mode 100644 Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting.png create mode 100644 Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting@2x.png create mode 100644 Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting@3x.png diff --git a/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/Contents.json b/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/Contents.json new file mode 100644 index 00000000..cf4072a4 --- /dev/null +++ b/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "photo_export.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "photo_export@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "photo_export@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export.png b/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export.png new file mode 100644 index 0000000000000000000000000000000000000000..427ff510be2bc821e4f9e9a9cfe0ef7e078b1aaa GIT binary patch literal 1057 zcmV++1m63JP)500009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yPP=^L06kTpVzIYGk-nm$1X2_$xy9TG?+CuleU^a&C+sCR@Dq?Wyd1)COw z`#rlHIc}G$oZ(58c=%QI>ervjo;1YK(NX04{@2gX&yMGLVUi?a{tovF_X_XFc>nA2 z@^T~%;JFCJaom1=ef@#U9@-e~9+QqgK0e0%eqZHsy_ zxw6gUxsbr12blb3V`HO_XD{N2larJ1{rx@0O_JK*;?{>I5+{GxkL0gtn^#n;d!gvH~zBX(Q(%+OGWqSu#N2O0ptM znh+sqz={=GlTO-Yql#z40ug601CR$@Tdl9J+gz_WDboVgKh*XR>G1F`<2X5j6AUYuD^r271wxMlv64B}o^H&6EMRdM?%# zWiZ(#^CPZfL3=?dk%o@kwFi0TtOB&hI=Hs7uK=}qh`w>15Gw}JQ9UiL;7x60?ZF*RX1Qs1Kl@u)`O2^NzhuE9`L3Sm~{pI zI{G-Va==a)BBH$HoFPN)u)ty*(Z`il66w&e%LUy`Zlbz)p5ZZKRfO_$8IMC{`)rkk zA08h5?Ck9DU_ad6-u{dG-?BU++!~t0zSD^^N8zmP{1w467M-Kh_^e$73_6zVe0<%M zRMW2*PTyyWUlEwlB&(T6c&5tVXP9(hb1Y>e$zsyL^;3!ynIc%lvv_9BYWi};q{m#- b^vl&hw)>GpN#Vv*00000NkvXXu0mjf4&?G- literal 0 HcmV?d00001 diff --git a/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export@2x.png b/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fc58052ebb2f4584300459cb79d2a1abf2d47e46 GIT binary patch literal 2143 zcmV-l2%z_gP)uIx0XCpvVvP`>1!ynWAWsM^d!=0rVXy(s%s$?%lh0?`}a##KVUVmys+lEG*m} z4u>A|ysr5%@OvN00M`e#TJ03+8O~3iJb7{|C1F7dM7!N?ynp|`iR?WYk)P}>Xlgyl3AqqM2|?u!4I!*3@<$+}>q^EnSS0P)-YXT7Q4+!}e*}FJ?T3cKDgM>xz zi)~71gQoxQRn<;$0pE#+DY6Y6(oCQpy}%{}F>9(`>4*c=5cF}}7I*Ngp5vg~!r4@S zyJHI~!bjHUAQ)r?_V4wWXN*$HJP1>lz_vvEF?oT$93&k|v}6R8F$m#@e@4#$(p&cL z7wj07mP1Ml47gj80el`wGje&LhYK1q=>cSuNIed+fp>nZ=E#;k;EYPlSXv|lP{1?! zMhb~Iz|qqogfn#1?8~U0%(rljx^PRfgYSB%|FdTLo3JpbnrZ zUWnbJ1hLmNuFg$WrkP-*2HAC^YsS42hUfP+n^oZ&Y3s|VqJcEV_ zmTQbWV2<<1QV>+B({%4!m#G_eT*fQ z3qf732$Y1jb;*mI=ZeY`qhl%C&r=F&W^BXoE~yw)|8elyloDX3Z^_A6Vi@x#6@o>I zCnW)1hVht>#L(Icg8=25lGv$_TtvqE{y{Y*e#v@MfgpXbMF4WH>X`a*n}6Eti#90i!B zM03~|cZ^?NrDPnqn1H6_gLmQxE4VSJX#fzz)4j_=BNAYA6NeX`O3523_v`n?0oo&l z`j4C#%Qt*6TfHAu*BZgO+4~t_qolH}1gI*%dc^WRI>x)=@}iuRWe^O0pX> zZyce_Odm3WaFmXoxo0kniQ$~B3H#JAvrN#6a+19-#UYTT}H3^TKl6c04v!Q=QvvwIFbROO)t z-PCxX31T!pgh%7UhYyExO~C+6)h)far8}`c-+H}Hso^kTLsxNJE9hFQCbENR0hqCI zzKnadV_{cUSN~#stXuPI?%LYgNnD`t zRf1VCg6xlYrvo&h{b~0mPM_ePv?ayDgerq|w^z}CltdMgNKABL0+jKKt4I#(%(`0( zx;8I+W~LQxE-S_64STs_n!0CR`gaw`(V(c~!j}FKW*MER&Vj4qO_jTH?UFAnS1e?) zsEys-U52WMv9)~GsPY#WLg}u+x(o#F^A~cr;=}irhZ_I0jeoUQmTO^;y76xt6 z$jhp1VE_cQ-siG~fsL5G75YdzMegyUI|gjiCo{ab*~V6M?NJsKzD<0h< literal 0 HcmV?d00001 diff --git a/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export@3x.png b/Projects/DSKit/Resources/Image.xcassets/photo_export.imageset/photo_export@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a177f09bb3064ff55e79407d09c59e14524cdcce GIT binary patch literal 3435 zcmV-x4V3bUP)C$0-UznZkcYo^)^AiVAD--U6Q{ba<^Bl11CVP3Izm4-~@@??Nye$yDE0K zSA_-=J6)9Sx^TO@>Kw!l_F%`}`H}PJ=O;>{BtDAz_yGhzB~kpnnKy6VaHd2<0?+d* zhlhs_Z_buVr60b2{aWF*g9YbLPfs25Gkh+Pa{+Jf^ZEnt?(@>Pbm>xqzjL3403|9Y zgp@lcCnxjea@kEHpw8zd@BjjU68!2RN~|wicKdFD)%O)6>%x zzAjw&Goh*!schQ_L{-*`AYlU|2~xuk;vPS+JG@&-xFqa5c-baPeTYad$hu8!8VKKK zoyn>Vimcp8k_Ypck{=2>_bq-Xn|XKakm%2!KcnHG(SWq3hBGL#awF-{qel*_rVUCa z$LD36iK!RC;!;KnH<){raKCwzWt}9g&+!sY`@GbiKY#91LGke6!&xS4i8t>hoc6e3 zt=n!`R!Ne6XTvm~`Pde>#QQ}xd6OWTz$1e!F<=&LHZ&_FiS(e~Su#%fRB*}Z1R?7c z8P*|><<@3qW@a8le{r5NLS~Z0{SVZe zryUwO>1hT2?@;Ub|GL@k@qe#T2FQdYzR8!Q^*^@6zQ{;Y^XR8mS6A;-pXgiNv2MoL z=O^uu5B2us$&>%k7~##EH~UwvT-oHt`91Gqf%p9?t3HQS-!}Duz9b0*@r_jDFDsSS z*)h39V-D6V9!2Ue1O~tSTo$QE^ejmxK@{~rk7xoI75??>*C7C|9GdM%qIx1p`XF7! zfgq12h>=9p&6_tXZ{NQC7j=oQNfIkgmzW7bEnpIbK8p8oe5Z@A>5e2B!09ISgrE)p zQP-|r`-c0at8&QY{a|lz??c+}NNqTXJLEmdSO9}m?hNnJ1gXFp=rx_GzWu4GsUOo0 zQOap9f-eqZ06HTmxOnkm&(_K~f*bf<6&S+^pC_Jcn$M;p3C>b&khYJ3>aaxG6a_rJ zA3isVJTYN()0Zz_9;f+i$397BQh(77{f$S_ztRL<0|Q<*9RAOnf5E;O5)1?@i9 zwoj57%J)i&`W=)_5P=P_R}$Nyx{e>)X{NU74l}A6LGZ_A;xHU{9335@p~q^EpL1I; zO-o4rBiy*WlUf`rfgTNX$kNFvg?8Bj`K~&|pKjL0&M9Z3xDI8zzOOQ??5=nv- z>3%s@b5_Pf{2OQpAg4;nv3G+&l_IiISzdiY?><~H(wZ=pbkLBZde-woqS_EA;I_Fe zki{k(YAvxI->1uKjBl(AHxTQ=M#PpfA}I2l`{s2`eUjN1wbPZof^SM6eJkKWZ1RHX z_lB(p)8c+yiX>>Bnn^=HwLq3&K5xc?8K~2w?sAZ#r0CUkUM}%+PyTz_njlj?29ORc$q;uWY0KU6LJRb!GL8aWSK*a$CizW`1hdt zOn1>al30pdZP&N(`iI#W*ZHA%R4~XrnGMvMAtCC`m)(jvZYsCB5d_UshLDz6)eeQx zL{waOBm+*t9Wi)pL7%mq*H6SGNzAh&yZh{_*rIIV8v9#m297e0ddUmSV&HuqV;NDu zdP7x%Z7X2op-yLw;+#1ilZ2o?I=jIw3tsMdW51)MG7$*$hvftDcjPbzx>d?&EMqp* zS+WFp$?3c_xoRUOiHVulw)+Y}Y5_;zfkoBsVliMk1y*|67{dn4K6#$(qOLah`P^39 zAxW_6NNXiCGc!JA4@cV$Qe2-_N?wvBh{_AJLsq>atn$z(l;d?)S$QEzrd4NYLeJ5* zgLW6n;L@R#-z(SKLZJT^S%j#(SbFgy8`etQ>0)d{)pd(*b-%T~VgQJrKhh&$9g<|c zj-U0|XoBt#KY!$}2-izXOVE2UNdm*k(b0sY0-~(D{FBm=O^`qIkOlCQDqw%q0pA~j zV|e+W%vt3Yf0qVRH0F>wE7MAJC^A;y;2d}tFxDht#+RD&AL*!20fIKik|Y^+AgtHs zgkfeV>jNj`ETs{L^fL&~EG$7X2^#|ZjFsUaQ!*f^(DP5od9Lr5_0cU4q8u7gNGt>h za;V_Yy+Ggxk`#Nsb??I{K`c1DATV$vAPtQ8zEAMo?K3&*q(cRR#4jLnMyaTPm(8?u zND}tt(GUUo%eCFc6g=PjN5VZ`e@6v@w5FsKp+FTu^!#UP9X<~eyGla_NieT=_o43e zIrTzxs3JW`?9pxv<4H8D)8g~!f2uTuF#c1c%udxnJ2{&+0Yslj{EgG7Tmf2t+72F# z>Z7=HHe`8CLtw#mKz#2=VqeCbx;|DqI5@EN=K^iJiKLX$(+Vy{Qd6bQ^)C8BK$fA$ zfFtGvF_0>xOV1i*izM9#<|qrM2k99k0$@VbyB!&16Uc3@_ua%FJYtMm*40ojNC z{6pahN#cHKOFLn!U-FH8NDl~dXn3$g79lZ^uG&XgAgn`@j99OVrG5zw;hf5&;e$^W z!KAoq8~u@*{L_7RaSZZlRP7{FNc=<*;>>$Ry<|CC3J-yrR9$#nJLszo)DW_y@;n+| zSPjC2sx#iTs;iBdq=c7itCTTR2{sx&)ND)9^z<|c3IzkdrN2~ePF3g~qZKPp0v>%D z9{72@A#Zds=Nj@kl8%p$*QmwIwHX7sihL?Qe2VVXD)3S{C0KO|=SWI;xz@^y!!RNw z6-<#;A5`2zP64AotBs^AG)XhFb#2?#r*gA~5$jz9fRH*xapoT7fP{=_O%1n+B*TEf zRIarbhXE~B4C)XeqvE>!en9pSP&7VVm){0Ci=~_r69Q>+IhB$=D1Fgu?a;z7mv}2G z(q=?U3`8-di7e*T8D~gsz!0T28PQrs3o)yU+O8`(Lnp`xQc@yzzqLz3+%hk?(<|W-~XK^=nG;jAUIQbb!w}tt6QzEZyR@qag(09|M2i|gC^)3Fc$Pw-aPxd z*6#YY8TQ}3d-pp_6Xt=ca_D>B|FpZi`;I2)39ROjYh5pGba!QC<=^c-)3FQ!QuG_8 zWv$&QJUKuFA!s zRCE?K>c$BPm8`q2V%>G&#iSZ_6QYt~5+sH~*JWONDq*^}xA!X(1MjsOF;9pxkRU;u zOwdo%19~n7%!F)Bh)Rak;ngJu1XZa=l&DYe4b^!SPyHfs6N)m9W6eL9*<$s&+Tp$k z0{SKiSYBR+`$(T%MAfo|tm|A985pQy9HhSykyS1Dp4FWW>7#UKYz2A7QDkr&Tu5;B zh$rD&?Qmwb;4_gV1-RcO_&)YVV4ehS_)(N9DNMOx#LMk7VfSCWcu}W}kSR$(3$YJ) zRkK+WFmH&X)T8qa(u$hToFo9zn0&LI-cN-;Rw^}6dTyAdN9s9L%cLpTPQtzY|&;6h*gpRNEJ`T!@_BblaTzW>lW%XWrkLgBdB4* z&h~J_I!Ou=8RL;QrqX~=-c6JPD#G|m#yeGtLap6|OMJwD!U8QXl zHOZ4OAqF+1?%}NxN`Cp;r^vMSDM#c^l0pZv6nu)po*P*|7)1(2ZY`C}BZ-)>;V^ld zgNi*I7)c7!A-j~KxS>=4Ku7%nNkWi)8L`^P9h;PzRAn7Q&LpJdk*a3jAzy`2pCbuA zFFSekIh`d5XUWtYB%%eOtYOKJ;my49 zT5qM@mG%R0Zy;h!j6C*;HTWyYxl1hZh)ijjvRA|eu7L``&M9#&|6L)Y5UrB`acJBpsERb*y}HH-V4=Qt2ESJJ+Hwy00dTJY)cuf)oNF1Bb0Z95@A22!zz5@@WsM zQ%s*8@rlUocN2tH^i1UR%o>eGq2w*`MJDw@EJ)vKXU^|VmbtUJr2iEQ>-Bn1!OHZR zm>7u`=XPf(ZJ*!KXoRg+YtP|uh;?;RlU-2P7c>VwyvIauC!d)OANiHYXs%2It_X Um)ZUNtN;K207*qoM6N<$f+G>-u>b%7 literal 0 HcmV?d00001 diff --git a/Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting@2x.png b/Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7077297b9827932fc33f8a2bbab6b6e7d615cb42 GIT binary patch literal 1043 zcmV+u1nm2XP)5|pyU3RHokig`y`xCWU_mJ@5kU$Uq*v4QoXaj0O7iimrr?X-0Ewe~l+T7;0mC=85+j zyI&A>A5#bo-Jyv^)0sAKZO*8swK4c9Gi)j7Eg&S6!qn^lh0l$(d$}OVks&LCFNILR zw$CpdyqRV2nL~Ct%2&X)ug*O9XNJ+#jy>ceCi4Uy_zc70Twp4;Kd8qk3?80A z{P2sl6BrK{to<880lSMcc$mXFigCmc5{6HoLtq$_U5LPaGJK+`Yoi7gc*41e zv5wp1_k7kL95PeQA2EI^^eT%O*GYMY*1(+(2>#H%c!}Mjt3m0I&9NUjTIwC_udzz> zDKW5)!=+^aW#0fiWJBoa$W9Ce*U)wqLZyj9=~AlqwNhGmQS;yZ{XINBKEh_Rfs>Py zS2A=S_w609E+Z1HFEU<}Tb)WmGsD^xJb}z@US3{sKA+>^;bDu?G<|)?kPpyrmM5`` zX^3*3{xM0C&fps&!wh&wGi=vM+nS1pQLgL{_+wiek=xwf-u5!mdm@G*`9uukL-If$ zS_Zg4>w+PI9TCESEe;M2oIJFsLn4F$Rns^+I&v}_(P&tb62J}MKK2d#anm$}aM{Ay z+1V}ze6_yCZpE2_Cy;FiDO6I(lp@d$VKnIrqh=zbWE30nm8PF6fvkEups270Baso& zx**pQ64|L#8);psqVFIlvKu8ghn|7_ccp}+gbK2yMNwgql};Bpa@Vi1uv|#8 z&a+W%ajb~@?6IxP)iN`9%MlgSpZ?h*L%eJD*AGQ2M+xo^V`ftb45jm&>A&%n8OF4cqnIDgI#1?y=!fq~N*@{DH8AMJ z4Bze$8kZ);cjXSB**?^au?zrxx(N}AtX@Fl2?r( zSVxhPy(>|^;Gj0Y6uA`ihP~imMo70QCCb-D5tEaym~xcQo|ZVq6_YWT0_|7`a+yP> zz|aS}4ntDx&N|Jg?V2@1`D%8^=DvlH7u^&B?_3w%hRnxZbnmf8jlW#?=mFI)BZdF~ N002ovPDHLkV1m;e+EV}k literal 0 HcmV?d00001 diff --git a/Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting@3x.png b/Projects/DSKit/Resources/Image.xcassets/setting.imageset/setting@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..02c526b053dfebff284ef86bc5c79815b222e539 GIT binary patch literal 1564 zcmV+%2IKjOP)_`xRi`B+384)~hj`U_FB9iq&Lbk&Rb~pj=2FyDFaDw5T0A5Dsc?l`A61lx_0N@0* zUAV0sdaCW}ZvQ>WpVUI*pK^7%f2zve01ONa3=H&^4QLQV9H~EJRbo}PRC!Znv9YnS z01uSBSj+pQWi6GgTlg7Kl{r)81$k0crVg@VeNV8g&e4^5KpE!(dFMh|X|D~Xl#5hJ z(49$%%qhAvCtaW~C}l<{d&@5+$43^qDf)B4$!!(PTTNAWniq=d>r<{WjO+{MI=VMr zk#&vb(De~?MTK#aqB6!kR57kKUFdTo*u2C?|E0J#y4}3O*n|UD^;gW6X{2%NOA@#wa%uaRDx%JW)<0C>%2|%s2&7BiXW- zHdrHVFoLPFw!t>je`$Q6>GM|Fk1_8eY&dd%e-C$eckulD3~z65u(!7d$H&KTaByG~ z_q%$oXuQLGoTJwLS(OO#G08Tw{aINb>&#BG*d3LO6e!2To5<-@8;5 zbj{h>8F#AkqoaUt%&}4FhH7ft{TlaD{v$hQ^cg zjT=ZgUKtn_At%rk=AKunvlxPDAnuJs5YC5+kcV92C9grPhq*o|5vHlk6`2Ogm}~xq zaMpaOtE($fSEFm@b2im|3*m6SRC%kkc$YV!Qz|EyJmjOJBdZrpVTi?x4)X4G#2^Ya zxL`F_e{3y47n#ye~T^^SuQ^j zEeb{ca3!L{IwI^QW2tA2<=;`+Mhtsp0-G_V!%eXT+ZlWud3M(@c$eRgerPzbbG-9wkpY2qc3Zqx% zl|yLspPLYv$3T?7rhN84kcSWp@BshMjnmjtEHqyR>`WXO7#JA%nfVW0$s%HijwA&D O0000 Date: Sat, 2 Sep 2023 21:12:02 +0900 Subject: [PATCH 13/30] =?UTF-8?q?feature=20#89:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainPage/MainPageView.swift | 66 ++++++++++--------- .../Sources/MyPage/MyPageFeature.swift | 13 ++++ .../Features/Sources/MyPage/MyPageView.swift | 23 +++++-- Projects/Features/Sources/Root/RootView.swift | 2 +- .../Sources/Setting/SettingFeature.swift | 39 +++++++++++ .../Sources/Setting/SettingView.swift | 45 +++++++++++++ 6 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 Projects/Features/Sources/Setting/SettingFeature.swift create mode 100644 Projects/Features/Sources/Setting/SettingView.swift diff --git a/Projects/Features/Sources/MainPage/MainPageView.swift b/Projects/Features/Sources/MainPage/MainPageView.swift index 5880737f..3609f971 100644 --- a/Projects/Features/Sources/MainPage/MainPageView.swift +++ b/Projects/Features/Sources/MainPage/MainPageView.swift @@ -25,38 +25,40 @@ struct KeymeMainView: View { } var body: some View { - WithViewStore(store, observe: { $0 }) { _ in - TabView(selection: $selectedTab) { - KeymeTestsHomeView(store: store.scope(state: \.home, action: MainPageFeature.Action.home)) - .tabItem { - homeTabImage - } - .tag(Tab.home) - - MyPageView(store: store.scope(state: \.myPage, action: MainPageFeature.Action.myPage)) - .tabItem { - myPageTabImage - } - .tag(Tab.myPage) - } - .introspect(.tabView, on: .iOS(.v16, .v17)) { tabViewController in - let tabBar = tabViewController.tabBar - - let barAppearance = UITabBarAppearance() - barAppearance.configureWithOpaqueBackground() - barAppearance.backgroundColor = UIColor(Color.hex("232323")) - - let itemAppearance = UITabBarItemAppearance() - itemAppearance.selected.iconColor = .white - itemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.white] - itemAppearance.normal.iconColor = .gray - itemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.gray] - - tabBar.standardAppearance = barAppearance - tabBar.standardAppearance.inlineLayoutAppearance = itemAppearance - tabBar.standardAppearance.stackedLayoutAppearance = itemAppearance - tabBar.standardAppearance.compactInlineLayoutAppearance = itemAppearance - tabBar.scrollEdgeAppearance = tabBar.standardAppearance + NavigationStack { + WithViewStore(store, observe: { $0 }) { _ in + TabView(selection: $selectedTab) { + KeymeTestsHomeView(store: store.scope(state: \.home, action: MainPageFeature.Action.home)) + .tabItem { + homeTabImage + } + .tag(Tab.home) + + MyPageView(store: store.scope(state: \.myPage, action: MainPageFeature.Action.myPage)) + .tabItem { + myPageTabImage + } + .tag(Tab.myPage) + } + .introspect(.tabView, on: .iOS(.v16, .v17)) { tabViewController in + let tabBar = tabViewController.tabBar + + let barAppearance = UITabBarAppearance() + barAppearance.configureWithOpaqueBackground() + barAppearance.backgroundColor = UIColor(Color.hex("232323")) + + let itemAppearance = UITabBarItemAppearance() + itemAppearance.selected.iconColor = .white + itemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.white] + itemAppearance.normal.iconColor = .gray + itemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.gray] + + tabBar.standardAppearance = barAppearance + tabBar.standardAppearance.inlineLayoutAppearance = itemAppearance + tabBar.standardAppearance.stackedLayoutAppearance = itemAppearance + tabBar.standardAppearance.compactInlineLayoutAppearance = itemAppearance + tabBar.scrollEdgeAppearance = tabBar.standardAppearance + } } } } diff --git a/Projects/Features/Sources/MyPage/MyPageFeature.swift b/Projects/Features/Sources/MyPage/MyPageFeature.swift index 5dd4b8c6..136bd75f 100644 --- a/Projects/Features/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Features/Sources/MyPage/MyPageFeature.swift @@ -22,6 +22,7 @@ public struct MyPageFeature: Reducer { var differentCircleDataList: [CircleData] = [] var view: View var scoreListState: ScoreListFeature.State = .init() + var settingViewState: SettingFeature.State? struct View: Equatable { let userId: Int @@ -44,11 +45,13 @@ public struct MyPageFeature: Reducer { case requestCircle(MatchRate) case view(View) case scoreListAction(ScoreListFeature.Action) + case settingViewAction(SettingFeature.Action) public enum View: Equatable { case markViewAsShown case circleTapped case circleDismissed + case prepareSettingView case selectSegement(MyPageSegment) } } @@ -123,11 +126,21 @@ public struct MyPageFeature: Reducer { state.view.circleShown = false return .none + case .view(.prepareSettingView): + state.settingViewState = .init() + return .none + case .scoreListAction: print("score") return .none + + default: + return .none } } + .ifLet(\.settingViewState, action: /Action.settingViewAction) { + SettingFeature() + } } } diff --git a/Projects/Features/Sources/MyPage/MyPageView.swift b/Projects/Features/Sources/MyPage/MyPageView.swift index 2fc6e0f8..16dfae79 100644 --- a/Projects/Features/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/Sources/MyPage/MyPageView.swift @@ -49,16 +49,17 @@ struct MyPageView: View { viewStore.send(.circleDismissed) } } - + if !viewStore.state.circleShown { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { - Button(action: {}) { + + NavigationLink(destination: destinationView()) { DSKitAsset.Image.photoExport.swiftUIImage .resizable() .frame(width: 35, height: 35) } - + Spacer() Text.keyme("마이", font: .body3Semibold) @@ -69,7 +70,7 @@ struct MyPageView: View { Spacer() - Button(action: {}) { + NavigationLink(destination: destinationView()) { DSKitAsset.Image.setting.swiftUIImage .resizable() .frame(width: 24, height: 24) @@ -108,3 +109,17 @@ struct MyPageView: View { } } } + +private extension MyPageView { + func destinationView() -> some View { + store.send(.view(.prepareSettingView)) + + let store = store.scope( + state: \.settingViewState, + action: MyPageFeature.Action.settingViewAction) + + return IfLetStore(store) { store in + SettingView(store: store) + } + } +} diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index 9e0e79cc..d9dd462c 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -16,7 +16,7 @@ public struct RootView: View { public init() { self.store = Store(initialState: RootFeature.State()) { - RootFeature()._printChanges() + RootFeature() } store.send(.view(.checkUserStatus)) diff --git a/Projects/Features/Sources/Setting/SettingFeature.swift b/Projects/Features/Sources/Setting/SettingFeature.swift new file mode 100644 index 00000000..286d99d7 --- /dev/null +++ b/Projects/Features/Sources/Setting/SettingFeature.swift @@ -0,0 +1,39 @@ +// +// SettingFeature.swift +// Features +// +// Created by Young Bin on 2023/09/02. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import ComposableArchitecture + +import Domain +import Network + +public struct SettingFeature: Reducer { + + public struct State: Equatable {} + + public enum Action: Equatable { + case logout + case withdrawal + case setPushNotification + } + + public init() { } + + public var body: some Reducer { + Reduce { state, action in + switch action { + case .logout: + return .none + case .withdrawal: + // TODO: Call api + return .none + case .setPushNotification: + return .none + } + } + } +} diff --git a/Projects/Features/Sources/Setting/SettingView.swift b/Projects/Features/Sources/Setting/SettingView.swift new file mode 100644 index 00000000..0a5d5315 --- /dev/null +++ b/Projects/Features/Sources/Setting/SettingView.swift @@ -0,0 +1,45 @@ +// +// SettingView.swift +// Features +// +// Created by Young Bin on 2023/09/02. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import ComposableArchitecture +import SwiftUI + +struct SettingView: View { + private let store: StoreOf + + init(store: StoreOf) { + self.store = store + } + + var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + List { + Section("개인정보") { + Button("로그아웃") { + + } + + Button("서비스 탈퇴") { + + } + } + + Section("마케팅 정보 수신 동의") { + + HStack { + Text("푸시 알림") + Spacer() + Toggle("", isOn: .constant(true)) + } + } + } + .listStyle(.inset) + .background(.red) + } + } +} From 1ad62f2327c3291a84f9c354801ef52334ab4f79 Mon Sep 17 00:00:00 2001 From: enebin Date: Sun, 3 Sep 2023 08:53:23 +0900 Subject: [PATCH 14/30] =?UTF-8?q?feature=20#89:=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainPage/MainPageView.swift | 32 ++++++++ .../Sources/Setting/SettingView.swift | 73 ++++++++++++++----- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/Projects/Features/Sources/MainPage/MainPageView.swift b/Projects/Features/Sources/MainPage/MainPageView.swift index 3609f971..d218d917 100644 --- a/Projects/Features/Sources/MainPage/MainPageView.swift +++ b/Projects/Features/Sources/MainPage/MainPageView.swift @@ -60,6 +60,38 @@ struct KeymeMainView: View { tabBar.scrollEdgeAppearance = tabBar.standardAppearance } } + .navigationBarHidden(true) + } + .introspect(.navigationStack, on: .iOS(.v16, .v17)) { navigationController in + let appearance = UINavigationBarAppearance() + appearance.configureWithTransparentBackground() + appearance.backgroundColor = UIColor(DSKitAsset.Color.keymeBlack.swiftUIColor) + appearance.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.white + ] + + let backbuttonImage = UIImage(systemName: "chevron.left")? + .withRenderingMode(.alwaysOriginal) + .withTintColor(.white) + .withAlignmentRectInsets(UIEdgeInsets(top: 0.0, left: -12.0, bottom: 0, right: 0.0)) + + appearance.setBackIndicatorImage(backbuttonImage, transitionMaskImage: backbuttonImage) + + let backButtonAppearance = UIBarButtonItemAppearance() + // backButton하단에 표출되는 text를 안보이게 설정 + backButtonAppearance.normal.titleTextAttributes = [ + .foregroundColor: UIColor.clear, + .font: UIFont.systemFont(ofSize: 0.0) + ] + + appearance.backButtonAppearance = backButtonAppearance + + navigationController.navigationBar.barTintColor = .white + navigationController.navigationBar.tintColor = .white + navigationController.navigationBar.standardAppearance = appearance + navigationController.navigationBar.compactAppearance = appearance + navigationController.navigationBar.scrollEdgeAppearance = appearance + navigationController.navigationBar.isTranslucent = false } } } diff --git a/Projects/Features/Sources/Setting/SettingView.swift b/Projects/Features/Sources/Setting/SettingView.swift index 0a5d5315..2781e2a0 100644 --- a/Projects/Features/Sources/Setting/SettingView.swift +++ b/Projects/Features/Sources/Setting/SettingView.swift @@ -7,7 +7,9 @@ // import ComposableArchitecture +import DSKit import SwiftUI +import SwiftUIIntrospect struct SettingView: View { private let store: StoreOf @@ -17,29 +19,64 @@ struct SettingView: View { } var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - List { - Section("개인정보") { - Button("로그아웃") { + WithViewStore(store, observe: { $0 }) { _ in + ZStack { + DSKitAsset.Color.keymeBlack.swiftUIColor + .ignoresSafeArea() + + ScrollView { + VStack(alignment: .leading, spacing: 30) { + section(title: "개인정보") { + Button(action: {}) { + item(text: "로그아웃") + } + Button(action: {}) { + item(text: "서비스 탈퇴") + } + } - } - - Button("서비스 탈퇴") { + Divider() + section(title: "마케팅 정보 수신 동의") { + HStack { + item(text: "푸시 알림") + + Spacer() + + Toggle("", isOn: .constant(true)) + } + } } + .fullFrame() + .foregroundColor(.white) + .scrollContentBackground(.hidden) + .padding(.horizontal, 34) + .padding(.top, 40) } - - Section("마케팅 정보 수신 동의") { - - HStack { - Text("푸시 알림") - Spacer() - Toggle("", isOn: .constant(true)) - } - } } - .listStyle(.inset) - .background(.red) } } } + +private extension SettingView { + func section(title: String, @ViewBuilder items: () -> some View) -> some View { + VStack(alignment: .leading, spacing: 0) { + caption(text: title) + .padding(.bottom, 36) + + VStack(alignment: .leading, spacing: 24) { + items() + } + } + } + + func caption(text: String) -> some View { + Text.keyme(text, font: .body4) + .foregroundColor(DSKitAsset.Color.keymeMediumgray.swiftUIColor) + } + + func item(text: String) -> some View { + Text.keyme(text, font: .body2) + .foregroundColor(DSKitAsset.Color.keymeWhite.swiftUIColor) + } +} From 3ab0e461a5ca85469c29b332995fb581c449386f Mon Sep 17 00:00:00 2001 From: enebin Date: Sun, 3 Sep 2023 12:53:36 +0900 Subject: [PATCH 15/30] =?UTF-8?q?fix=20#89:=20=ED=99=88=20=EB=B7=B0=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Features/Sources/Home/KeymeTestHomeFeature.swift | 3 +++ Projects/Features/Sources/Home/KeymeTestsStartFeature.swift | 5 ++++- Projects/Features/Sources/Home/KeymeTestsStartView.swift | 6 ++++-- .../Features/Sources/KeymeTests/WebView/KeymeWebView.swift | 1 - Projects/Features/Sources/MyPage/MyPageView.swift | 1 - 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift index c56da995..74ed6259 100644 --- a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift @@ -57,5 +57,8 @@ public struct KeymeTestsHomeFeature: Reducer { return .none } + .ifLet(\.$testStartViewState, action: /Action.startTest) { + KeymeTestsStartFeature() + } } } diff --git a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift index 3f74eed0..670a88cd 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift @@ -47,6 +47,10 @@ public struct KeymeTestsStartFeature: Reducer { return .send(.startAnimation(state.testData.icons)) case .startAnimation(let icons): + guard state.isAnimating == false else { + return .none + } + return .run { send in repeat { for icon in icons { @@ -58,7 +62,6 @@ public struct KeymeTestsStartFeature: Reducer { case let .setIcon(icon): state.icon = icon - state.isAnimating = true case .startButtonDidTap: let url = "https://keyme-frontend.vercel.app/test/\(state.testData.testId)" diff --git a/Projects/Features/Sources/Home/KeymeTestsStartView.swift b/Projects/Features/Sources/Home/KeymeTestsStartView.swift index f69d9742..ded1d02f 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartView.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartView.swift @@ -19,7 +19,6 @@ public struct KeymeTestsStartView: View { public init(store: StoreOf) { self.store = store - store.send(.viewWillAppear) } public var body: some View { @@ -42,6 +41,9 @@ public struct KeymeTestsStartView: View { } ) } + .onAppear { + store.send(.viewWillAppear) + } } func startTestsButton(_ viewStore: ViewStore Date: Sun, 3 Sep 2023 17:54:37 +0900 Subject: [PATCH 16/30] =?UTF-8?q?fix=20#89:=20=EC=9B=B9=EB=B7=B0=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=ED=86=A0=ED=81=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/KeymeTests/KeymeTestsFeature.swift | 4 ++++ Projects/Features/Sources/KeymeTests/KeymeTestsView.swift | 2 +- .../Features/Sources/KeymeTests/WebView/KeymeWebView.swift | 7 +++++-- .../Network/Sources/Network/Core/CoreNetworkService.swift | 6 ++++++ Projects/Network/Sources/Network/Core/CoreNetworking.swift | 2 ++ .../Network/Sources/Network/Manager/APIRequestable.swift | 3 +++ .../Network/Sources/Network/Manager/KeymeAPIManager.swift | 4 ++++ 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift b/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift index 4813a52e..17e3eeb3 100644 --- a/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift +++ b/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift @@ -15,6 +15,10 @@ public struct KeymeTestsFeature: Reducer { public struct State: Equatable { let url: String + var authorizationToken: String? { + @Dependency(\.keymeAPIManager.authorizationToken) var authorizationToken + return authorizationToken + } public init(url: String) { self.url = url diff --git a/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift b/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift index 63859764..9ea1ea96 100644 --- a/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift +++ b/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift @@ -22,7 +22,7 @@ public struct KeymeTestsView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in ZStack { - KeymeWebView(url: viewStore.url) + KeymeWebView(url: viewStore.url, accessToken: viewStore.authorizationToken ?? "") // TODO: handle it .onCloseWebView { print("close") } diff --git a/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift b/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift index d2c1ead2..fe24f948 100644 --- a/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift +++ b/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift @@ -30,7 +30,7 @@ public struct KeymeWebView: UIViewRepresentable { private let webView: WKWebView public let url: String - init(url: String) { + init(url: String, accessToken: String) { self.option = .init() self.url = url self.webView = WKWebView(frame: CGRect.zero, configuration: WKWebViewConfiguration()) @@ -39,7 +39,10 @@ public struct KeymeWebView: UIViewRepresentable { let encodedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedUrl) { - webView.load(URLRequest(url: url)) + var request = URLRequest(url: url) + request.setValue(accessToken, forHTTPHeaderField: "Authorization") + + webView.load(request) } } diff --git a/Projects/Network/Sources/Network/Core/CoreNetworkService.swift b/Projects/Network/Sources/Network/Core/CoreNetworkService.swift index c0bd3cdb..0e9c0302 100644 --- a/Projects/Network/Sources/Network/Core/CoreNetworkService.swift +++ b/Projects/Network/Sources/Network/Core/CoreNetworkService.swift @@ -12,6 +12,7 @@ import Foundation import Moya public class CoreNetworkService { + private var token: String? public private(set) var provider: MoyaProvider init(provider: MoyaProvider) { @@ -20,6 +21,10 @@ public class CoreNetworkService { } extension CoreNetworkService: CoreNetworking { + var authorizationToken: String? { + self.token + } + func request(_ api: APIType) async throws -> Response { try await withCheckedThrowingContinuation { continuation in provider.request(api) { result in @@ -51,6 +56,7 @@ extension CoreNetworkService: CoreNetworking { plugins: [networkLogger]) provider = newProvider + token = authorizationToken } } diff --git a/Projects/Network/Sources/Network/Core/CoreNetworking.swift b/Projects/Network/Sources/Network/Core/CoreNetworking.swift index 92ae89b2..04f5a941 100644 --- a/Projects/Network/Sources/Network/Core/CoreNetworking.swift +++ b/Projects/Network/Sources/Network/Core/CoreNetworking.swift @@ -22,4 +22,6 @@ protocol CoreNetworking { /// 인증토큰 헤더에 넣어주는 메서드 func registerAuthorizationToken(_ authorizationToken: String?) + + var authorizationToken: String? { get } } diff --git a/Projects/Network/Sources/Network/Manager/APIRequestable.swift b/Projects/Network/Sources/Network/Manager/APIRequestable.swift index 77d8e43a..c7740527 100644 --- a/Projects/Network/Sources/Network/Manager/APIRequestable.swift +++ b/Projects/Network/Sources/Network/Manager/APIRequestable.swift @@ -28,4 +28,7 @@ public protocol APIRequestable { /// 인증토큰 헤더에 넣어주는 메서드 func registerAuthorizationToken(_ token: String?) + + /// 인증토큰 + var authorizationToken: String? { get } } diff --git a/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift b/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift index 8ebfbe38..f9c44f23 100644 --- a/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift +++ b/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift @@ -32,6 +32,10 @@ public class KeymeAPIManager { } extension KeymeAPIManager: CoreNetworking { + public var authorizationToken: String? { + core.authorizationToken + } + public func registerAuthorizationToken(_ token: String?) { core.registerAuthorizationToken(token) } From 070661703137b8d8052c8d0d024ae26df44d6927 Mon Sep 17 00:00:00 2001 From: enebin Date: Sun, 3 Sep 2023 22:14:42 +0900 Subject: [PATCH 17/30] =?UTF-8?q?fix=20#89:=20=EC=8A=A4=ED=83=9D=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/Utility/Function/TCA+.swift | 46 +++++++++++++ .../KeymeTests/WebView/KeymeWebView.swift | 1 - .../Sources/MainPage/MainPageFeature.swift | 22 ++++--- .../Sources/MyPage/MyPageFeature.swift | 6 +- .../Sources/Setting/SettingFeature.swift | 2 + .../Sources/Setting/SettingView.swift | 66 +++++++++++++++---- 6 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 Projects/Core/Sources/Utility/Function/TCA+.swift diff --git a/Projects/Core/Sources/Utility/Function/TCA+.swift b/Projects/Core/Sources/Utility/Function/TCA+.swift new file mode 100644 index 00000000..43e9a293 --- /dev/null +++ b/Projects/Core/Sources/Utility/Function/TCA+.swift @@ -0,0 +1,46 @@ +// +// TCA+.swift +// Core +// +// Created by Young Bin on 2023/09/03. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import ComposableArchitecture + +private final class Ref: Equatable { + var val: T + init(_ v: T) { + self.val = v + } + + static func == (lhs: Ref, rhs: Ref) -> Bool { + lhs.val == rhs.val + } +} + +/// 스택 오버플로우 방지용 래퍼 +/// 참고: https://github.com/pointfreeco/swift-composable-architecture/discussions/488 +@propertyWrapper +public struct Box: Equatable { + private var ref: Ref + + public init(_ x: T) { + self.ref = Ref(x) + } + + public var wrappedValue: T { + get { ref.val } + set { + if !isKnownUniquelyReferenced(&ref) { + ref = Ref(newValue) + return + } + ref.val = newValue + } + } + + public var projectedValue: Box { + self + } +} diff --git a/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift b/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift index fe24f948..ff4d60e7 100644 --- a/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift +++ b/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift @@ -41,7 +41,6 @@ public struct KeymeWebView: UIViewRepresentable { { var request = URLRequest(url: url) request.setValue(accessToken, forHTTPHeaderField: "Authorization") - webView.load(request) } } diff --git a/Projects/Features/Sources/MainPage/MainPageFeature.swift b/Projects/Features/Sources/MainPage/MainPageFeature.swift index f832dafe..d0b8d2ab 100644 --- a/Projects/Features/Sources/MainPage/MainPageFeature.swift +++ b/Projects/Features/Sources/MainPage/MainPageFeature.swift @@ -8,15 +8,16 @@ import Foundation import ComposableArchitecture +import Core public struct MainPageFeature: Reducer { public struct State: Equatable { - var home: KeymeTestsHomeFeature.State - var myPage: MyPageFeature.State + @Box var home: KeymeTestsHomeFeature.State + @Box var myPage: MyPageFeature.State public init(userId: Int, nickname: String) { - self.home = .init(nickname: nickname) - self.myPage = .init(userId: userId, nickname: nickname) + self._home = .init(.init(nickname: nickname)) + self._myPage = .init(.init(userId: userId, nickname: nickname)) } } @@ -26,10 +27,6 @@ public struct MainPageFeature: Reducer { } public var body: some Reducer { - Reduce { _, _ in - return .none - } - Scope(state: \.home, action: /Action.home) { KeymeTestsHomeFeature() } @@ -38,5 +35,14 @@ public struct MainPageFeature: Reducer { MyPageFeature() } + Reduce { _, action in + switch action { + case .myPage(.settingViewAction(.logout)): + print("logout from mainpage") + default: + break + } + return .none + } } } diff --git a/Projects/Features/Sources/MyPage/MyPageFeature.swift b/Projects/Features/Sources/MyPage/MyPageFeature.swift index 136bd75f..91156e80 100644 --- a/Projects/Features/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Features/Sources/MyPage/MyPageFeature.swift @@ -21,8 +21,8 @@ public struct MyPageFeature: Reducer { var similarCircleDataList: [CircleData] = [] var differentCircleDataList: [CircleData] = [] var view: View - var scoreListState: ScoreListFeature.State = .init() - var settingViewState: SettingFeature.State? + @Box var scoreListState: ScoreListFeature.State + @Box var settingViewState: SettingFeature.State? struct View: Equatable { let userId: Int @@ -36,6 +36,8 @@ public struct MyPageFeature: Reducer { init(userId: Int, nickname: String) { self.view = View(userId: userId, nickname: nickname) + self._scoreListState = .init(.init()) + self._settingViewState = .init(nil) } } diff --git a/Projects/Features/Sources/Setting/SettingFeature.swift b/Projects/Features/Sources/Setting/SettingFeature.swift index 286d99d7..d0751a86 100644 --- a/Projects/Features/Sources/Setting/SettingFeature.swift +++ b/Projects/Features/Sources/Setting/SettingFeature.swift @@ -27,7 +27,9 @@ public struct SettingFeature: Reducer { Reduce { state, action in switch action { case .logout: + print("logout from setting") return .none + case .withdrawal: // TODO: Call api return .none diff --git a/Projects/Features/Sources/Setting/SettingView.swift b/Projects/Features/Sources/Setting/SettingView.swift index 2781e2a0..ff2abfcf 100644 --- a/Projects/Features/Sources/Setting/SettingView.swift +++ b/Projects/Features/Sources/Setting/SettingView.swift @@ -12,6 +12,9 @@ import SwiftUI import SwiftUIIntrospect struct SettingView: View { + @State private var showAlert = false + @State private var alertItem: AlertItem? + private let store: StoreOf init(store: StoreOf) { @@ -19,7 +22,7 @@ struct SettingView: View { } var body: some View { - WithViewStore(store, observe: { $0 }) { _ in + WithViewStore(store, observe: { $0 }) { viewStore in ZStack { DSKitAsset.Color.keymeBlack.swiftUIColor .ignoresSafeArea() @@ -27,14 +30,10 @@ struct SettingView: View { ScrollView { VStack(alignment: .leading, spacing: 30) { section(title: "개인정보") { - Button(action: {}) { - item(text: "로그아웃") - } - Button(action: {}) { - item(text: "서비스 탈퇴") - } + logoutButton(action: { viewStore.send(.logout) }) + withdrawlButton(action: { viewStore.send(.withdrawal) }) } - + Divider() section(title: "마케팅 정보 수신 동의") { @@ -53,20 +52,53 @@ struct SettingView: View { .padding(.horizontal, 34) .padding(.top, 40) } + .alert("앗!", isPresented: $showAlert, presenting: alertItem) { item in + Button("취소", role: .cancel) { } + Button(item.actionButtonName) { item.action() } + } message: { item in + Text(item.message) + } } } } } private extension SettingView { + func logoutButton(action: @escaping () -> Void) -> some View { + Button(action: { + showAlert = true + alertItem = AlertItem( + message: "정말 로그아웃 하시겠어요?", + actionButtonName: "로그아웃", + action: action + ) + }) { + item(text: "로그아웃").frame(minWidth: 0, maxWidth: .infinity) + } + } + + func withdrawlButton(action: @escaping () -> Void) -> some View { + Button(action: { + showAlert = true + alertItem = AlertItem( + message: "탈퇴 시 모든 정보가 삭제됩니다. 정말 탈퇴하시겠어요?", + actionButtonName: "회원탈퇴", + action: action + ) + }) { + item(text: "서비스 탈퇴").frame(minWidth: 0, maxWidth: .infinity) + } + } + func section(title: String, @ViewBuilder items: () -> some View) -> some View { VStack(alignment: .leading, spacing: 0) { caption(text: title) - .padding(.bottom, 36) + .padding(.bottom, 24) - VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 0) { items() } + .frame(minWidth: 0, maxWidth: .infinity) } } @@ -76,7 +108,17 @@ private extension SettingView { } func item(text: String) -> some View { - Text.keyme(text, font: .body2) - .foregroundColor(DSKitAsset.Color.keymeWhite.swiftUIColor) + HStack { + Text.keyme(text, font: .body2) + .foregroundColor(DSKitAsset.Color.keymeWhite.swiftUIColor) + Spacer() + } + .padding(.vertical, 12) } } + +private struct AlertItem { + let message: String + let actionButtonName: String + let action: () -> Void +} From 07be2dd7b4f8789e0a3d29b998dfd7addaf141c6 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 09:49:08 +0900 Subject: [PATCH 18/30] =?UTF-8?q?feature=20#89:=20=EB=A3=A8=ED=8A=B8?= =?UTF-8?q?=EB=B7=B0=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=95=A1=EC=85=98=20=EC=88=98=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GoogleService-Info.plist.encrypted | Bin 1136 -> 1104 bytes Encrypted/XCConfig/App/DEV.xcconfig.encrypted | Bin 256 -> 160 bytes .../XCConfig/App/PROD.xcconfig.encrypted | 2 +- .../Features/Sources/Root/RootFeature.swift | 4 ++++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Encrypted/Secrets/GoogleService-Info.plist.encrypted b/Encrypted/Secrets/GoogleService-Info.plist.encrypted index 58ede7d001cfafc39c7cc8ab647682ca495f8654..dbe4cb732ff1ca1e93aa85b1713bd24bea66e0ca 100644 GIT binary patch delta 910 zcmV;919AND2+#{Z z|HRnX>JDy2`Vh}tG66C1F>bQZ1$6@sGEq+-*$w6j9l)uOLLxT?>+jKjRZ$8ur;hDotqED;&CGzW*9hOh_@l(FY5!frsxewvWZ z!wwaG=*SmIO8+sFXSyc3ll~!Pr3by1uy^RQ=vFzP%YSDRC+f_E*MgDB8O*Y5ifRUL z`pD$pFG7b*ReBXItx2^%6?zvXFO@3kD~nr{)Jm>rVmjVGL=6sBm!R{5Hc{WoQKI23 z=sN%eFIyLI4peX3NZE(c1NYM!O^PYlek$G6edcWv@cw(wH_cyQkp;4-;~nr^)yP3Q zARFgHCx3|z47Y+rkE}*Uw~#A*K=#@clCb@L6)9Epb$h(GGNOe5r#`f+Mz!k-Vm9Hr zFZl!j3@%yCNA(JnY`qYlS`-EIimA&IO2aXjUSX^ZhpmO~1@YNcLYeoz;8r<43~?_` z^L^_lv`C)>QLEi3Bu-GpCkk3XquC@dT?3O_`G2RqN4UtbE!dzT47xtnhSxqF-!)&0Av-@h5}csr&jJ5dj%HuW%4LJ#3QbWKbVJ;Wxw8y5N3-B)8Abe4 zC@F8Uh}FxHtCFYLm&y$iZG6(`{Jgk5V*o7vJsIp#$~IZbKlKFd95u%WM(Ozyy|ZIM zUVk4wn+PA$D?JR?^^*Q1a_#I#1pqnEE_Krn4(`wIJhB_dzMw}1C^b{3FPLT7T|wOV6O z3xMs)8k+x53px32IC19*{^m|q~5vQHB~0)@iUdx-VX?xnLu5g zEH%~P%!t*(Rnnm`_9-zzb8h(S6zOo;D6iS#&#DkA3E=@T}e>^fwT9@ zpwea;(3hN>g^%ZCXq=O^2kmbP)Kh?vG`PAQo%JU4_{j-blCst#tZhh|%_}k4m}gml z9lRCDn~ctd5Vgs~0_?>=fjs{^lth#1+FLt3k=gB^U>&R8M6VsgWIJUiP-R^SHUHUuT`p_hmwztB?fML zUVs1j0PHs7e4orm!2dEJ%le2@(N1R8rIw^VoA@0@C{EtjoBl>mY10blHOYMc@Jwph z7$BliN*3kD&-m+%99_+=$^Sf&qmFS{`o{om7zEb3e~a4i0w2`tUfxe-nSrK>BN6(1 z@9cv{5;lM@#yPmhS%;btXbXi!ROJV7n|+dV=w7lVj3sbmhC>2LJ#7 diff --git a/Encrypted/XCConfig/App/DEV.xcconfig.encrypted b/Encrypted/XCConfig/App/DEV.xcconfig.encrypted index da673e05876f366546d7faf65f0265ce1f670dec..44249a24f6a477e745f4fbc590d1999cbd440ddf 100644 GIT binary patch delta 87 zcmV-d0I2_f0-ynqP*93q(j>+Jh}nIbbO)OT9|#D^UrMp%bj+fr|9<;>9(%E7tMS@J twFget`4QZ?860O4KuhLLbX5+|gf(e(0J0og0L6(|!Swn%MgJmHEW*$QC@}y4 delta 184 zcmV;p07w6z0e}LKP=E3E)Epm1kx_yI>O>D==bhaxG3!J!MHs2ZBt#_ixOiEF#tZZI zwhcRbPYe~rgeM6hJHbbnj`KJy(py>Qlg%G-5LC4Q5Cl^)WaiWYwasiXByz0c5aW)A zhmE|cNrksLX%?&1zNd&9_&rLZS|kPECI@cEN6^hfH^yErpD|EL$Gb#Jm?fixXhd9~ mod-^-BH+oc`hw<0o}>O68I4S8Cj2^;0O*W~P^Ea~kH|m5u2j$f diff --git a/Encrypted/XCConfig/App/PROD.xcconfig.encrypted b/Encrypted/XCConfig/App/PROD.xcconfig.encrypted index 5a5edbbd..702bced4 100644 --- a/Encrypted/XCConfig/App/PROD.xcconfig.encrypted +++ b/Encrypted/XCConfig/App/PROD.xcconfig.encrypted @@ -1 +1 @@ -dBJؠLH 8v2cmH "?^F1Xno^ |т2x>SFgtmz \ԌuQ&=OpVfazuc)!wW0+SXl>7{mD@Z]增o_{ZEx6 l}5`W.?ho>Hˈ4: SѢC^7L \ No newline at end of file +dBJؠLH 8v2cmH "?^F1Xno^ |т2x>SFgtmz \ԌuQ&uD0p>y|/N&UcŞ;RV$HKX6W]nu.i}Dş?~Jod >wR \ No newline at end of file diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 89c3738b..205ab52d 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -167,6 +167,10 @@ public struct RootFeature: Reducer { return .send(.updateState(.canUseApp(userId: userId, nickname: nickname))) + case .mainPage(.presented(.myPage(.settingViewAction(.logout)))): + print("logout from rootview") + return .none + default: return .none } From fa4249333b7dd187a2390ef5af4b55419b984b09 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 10:43:46 +0900 Subject: [PATCH 19/30] =?UTF-8?q?refactor=20#89:=20=EB=A3=A8=ED=8A=B8?= =?UTF-8?q?=EB=B7=B0=EB=A5=BC=20=EC=8A=A4=EC=9C=84=EC=B9=98=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20?= =?UTF-8?q?=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EA=B4=80=EB=A6=AC=20-=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/Root/RootFeature+.swift | 44 +++++ .../Features/Sources/Root/RootFeature.swift | 10 -- .../Sources/Root/SwitchingRootFeature.swift | 165 ++++++++++++++++++ .../Sources/Root/SwitchingRootView.swift | 98 +++++++++++ Projects/Keyme/Sources/KeymeApp.swift | 2 +- 5 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 Projects/Features/Sources/Root/SwitchingRootFeature.swift create mode 100644 Projects/Features/Sources/Root/SwitchingRootView.swift diff --git a/Projects/Features/Sources/Root/RootFeature+.swift b/Projects/Features/Sources/Root/RootFeature+.swift index 9ce98bd2..f9d8d004 100644 --- a/Projects/Features/Sources/Root/RootFeature+.swift +++ b/Projects/Features/Sources/Root/RootFeature+.swift @@ -61,3 +61,47 @@ extension RootFeature.UserNotificationCenterDelegateManager: MessagingDelegate { tokenSemaphore.signal() } } + +extension SwitchingRootFeature { + final class UserNotificationCenterDelegateManager: NSObject { + private var fcmToken: String? + private let tokenSemaphore = DispatchSemaphore(value: 0) + + func waitForToken() async -> String? { + startRegister() + + return await withCheckedContinuation { continuation in + // If the token has already been received before this method was called + if let token = self.fcmToken { + continuation.resume(returning: token) + return + } + + // Wait for the token to be received + DispatchQueue.global().async { + _ = self.tokenSemaphore.wait(timeout: .now() + 10) + if let token = self.fcmToken { + continuation.resume(returning: token) + } else { + continuation.resume(returning: nil) + } + } + } + } + + private func startRegister() { + Messaging.messaging().delegate = self + } + } +} + +extension SwitchingRootFeature.UserNotificationCenterDelegateManager: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + guard let token = fcmToken else { + return + } + + self.fcmToken = token + tokenSemaphore.signal() + } +} diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 205ab52d..f3fb0da6 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -189,13 +189,3 @@ public struct RootFeature: Reducer { } } } - -private extension RootFeature { - func needsRegistration(forNickname nickname: String?) -> Bool { - if nickname == nil { - return true - } else { - return false - } - } -} diff --git a/Projects/Features/Sources/Root/SwitchingRootFeature.swift b/Projects/Features/Sources/Root/SwitchingRootFeature.swift new file mode 100644 index 00000000..718a3d18 --- /dev/null +++ b/Projects/Features/Sources/Root/SwitchingRootFeature.swift @@ -0,0 +1,165 @@ +// +// SwitchingRootFeature.swift +// Features +// +// Created by 이영빈 on 2023/09/04. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Foundation +import Domain +import Network +import ComposableArchitecture + +public struct SwitchingRootFeature: Reducer { + @Dependency(\.userStorage) private var userStorage + @Dependency(\.keymeAPIManager) private var network + + public init() {} + + public enum State: Equatable { + case notDetermined + case needSignIn(SignInFeature.State) + case needRegistration(RegistrationFeature.State) + case needOnboarding(OnboardingFeature.State) + case canUseApp(MainPageFeature.State) + } + + public enum Action { + public enum View { + case checkUserStatus + } + case view(View) + + case login(SignInFeature.Action) + case registration(RegistrationFeature.Action) + case onboarding(OnboardingFeature.Action) + case mainPage(MainPageFeature.Action) + + case updateState(State) + case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?) + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .view(.checkUserStatus): + let accessToken = userStorage.accessToken + if accessToken == nil { // 로그 아웃 상태 + return .send(.updateState(.needSignIn(.loggedOut))) + } else { // 로그인 상태 + network.registerAuthorizationToken(accessToken) + return .send(.updateMemberInformation(withMemberData: nil)) + } + + case .updateMemberInformation(let receviedMemberData): + return .run(priority: .userInitiated) { send in + let memberInformation: MemberUpdateDTO.MemberData + if let receviedMemberData { + memberInformation = receviedMemberData + } else { + memberInformation = try await network.request( + .member(.fetch), + object: MemberUpdateDTO.self + ).data + } + + userStorage.userId = memberInformation.id + userStorage.nickname = memberInformation.nickname + userStorage.friendCode = memberInformation.friendCode + + if let profileImageURL = URL(string: memberInformation.profileImage) { + userStorage.profileImageURL = profileImageURL + } + + if let profileThumbnailURL = URL(string: memberInformation.profileImage) { + userStorage.profileThumbnailURL = profileThumbnailURL + } + + if let userId = memberInformation.id, let nickname = memberInformation.nickname { + if memberInformation.isOnboardingClear != true { + await send(.updateState(.needOnboarding(OnboardingFeature.State()))) + } else { + await send(.updateState( + .canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) + } + } else { + await send(.updateState(.needRegistration(RegistrationFeature.State()))) + } + + Task.detached(priority: .low) { + let notificationDelegate = UserNotificationCenterDelegateManager() + + guard let token = await notificationDelegate.waitForToken() else { + print("푸시토큰 등록 중 에러 발생") + return + } + + _ = try await network.request(.registerPushToken(.register(token))) + } + } + + case .updateState(let receivedState): + state = receivedState + return .none + + // MARK: - Presentation actions + case .login(.signInWithAppleResponse(let response)): + switch response { + case .success(let body): + let token = body.data.token.accessToken + userStorage.accessToken = token + network.registerAuthorizationToken(token) + + return .send(.updateMemberInformation(withMemberData: nil)) + + case .failure: + return .none + } + + case .login(.signInWithKakaoResponse(let response)): + switch response { + case .success(let body): + let token = body.data.token.accessToken + userStorage.accessToken = token + network.registerAuthorizationToken(token) + + return .send(.updateMemberInformation(withMemberData: nil)) + + case .failure: + return .none + } + + case .registration(.finishRegisterResponse(let response)): + return .send(.updateMemberInformation(withMemberData: response.data)) + + case .onboarding(.testResult(.closeButtonDidTap)): + guard let userId = userStorage.userId, let nickname = userStorage.nickname else { + // 멤버 정보 수신 재시도 + // TODO: and show alert. 사실 있을 수 없는 케이스긴 함 + return .send(.updateMemberInformation(withMemberData: nil)) + } + + return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) + + case .mainPage(.myPage(.settingViewAction(.logout))): + print("logout from rootview") + return .none + default: + return .none + } + } + .ifCaseLet(/State.needSignIn, action: /Action.login) { + SignInFeature() + } + .ifCaseLet(/State.needRegistration, action: /Action.registration) { + RegistrationFeature() + } + .ifCaseLet(/State.needOnboarding, action: /Action.onboarding) { + OnboardingFeature() + } + .ifCaseLet(/State.canUseApp, action: /Action.mainPage) { + MainPageFeature() + } + } +} diff --git a/Projects/Features/Sources/Root/SwitchingRootView.swift b/Projects/Features/Sources/Root/SwitchingRootView.swift new file mode 100644 index 00000000..9ee6d028 --- /dev/null +++ b/Projects/Features/Sources/Root/SwitchingRootView.swift @@ -0,0 +1,98 @@ +// +// SwitchingRootView.swift +// Features +// +// Created by 이영빈 on 2023/09/04. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import SwiftUI +import Core +import ComposableArchitecture +import DSKit + +public struct SwitchingRootView: View { + @State private var showBlurringBackground = false + private let store: StoreOf + + public init() { + self.store = Store(initialState: SwitchingRootFeature.State.notDetermined) { + SwitchingRootFeature() + } + + store.send(.view(.checkUserStatus)) + } + + public var body: some View { + ZStack { + // 애니메이션 부웅.. 부웅.. + KeymeLottieView(asset: .background, loopMode: .autoReverse) + .ignoresSafeArea() + + if showBlurringBackground { + BackgroundBlurringView(style: .dark) + .ignoresSafeArea() + .transition(.opacity.animation(Animation.customInteractiveSpring())) + } + + SwitchStore(store) { state in + switch state { + case .needSignIn: + CaseLet( + /SwitchingRootFeature.State.needSignIn, + action: SwitchingRootFeature.Action.login + ) { store in + SignInView(store: store) + } + .zIndex(ViewZIndex.siginIn.rawValue) + .transition(.opacity.animation(Animation.customInteractiveSpring())) + .onDisappear { + showBlurringBackground = true + } + + case .needRegistration: + CaseLet( + /SwitchingRootFeature.State.needRegistration, + action: SwitchingRootFeature.Action.registration + ) { store in + RegistrationView(store: store) + } + .zIndex(ViewZIndex.registration.rawValue) + .transition(.opacity.animation(Animation.customInteractiveSpring())) + + case .needOnboarding: + CaseLet( + /SwitchingRootFeature.State.needOnboarding, + action: SwitchingRootFeature.Action.onboarding + ) { store in + OnboardingView(store: store) + } + .zIndex(ViewZIndex.onboarding.rawValue) + .transition(.opacity.animation(Animation.customInteractiveSpring())) + + case .canUseApp: + CaseLet( + /SwitchingRootFeature.State.canUseApp, + action: SwitchingRootFeature.Action.mainPage + ) { store in + KeymeMainView(store: store) + } + .zIndex(ViewZIndex.main.rawValue) + .transition(.opacity.animation(Animation.customInteractiveSpring())) + + default: + Text("") + } + } + } + } +} + +private extension SwitchingRootView { + enum ViewZIndex: CGFloat { + case siginIn = 4 + case registration = 3 + case onboarding = 2 + case main = 1 + } +} diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index fff8c7f6..1b80b221 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -17,7 +17,7 @@ struct KeymeApp: App { var body: some Scene { WindowGroup { - RootView() + SwitchingRootView() .onOpenURL { url in print(url) if (AuthApi.isKakaoTalkLoginUrl(url)) { From c80e8755b6d70c970c9b6b3bd6c6c2b1647f60a0 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 10:51:59 +0900 Subject: [PATCH 20/30] =?UTF-8?q?refactor=20#89:=20=EB=B0=B1=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EB=B8=94=EB=9F=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Root/SwitchingRootView.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Projects/Features/Sources/Root/SwitchingRootView.swift b/Projects/Features/Sources/Root/SwitchingRootView.swift index 9ee6d028..328901fe 100644 --- a/Projects/Features/Sources/Root/SwitchingRootView.swift +++ b/Projects/Features/Sources/Root/SwitchingRootView.swift @@ -29,13 +29,15 @@ public struct SwitchingRootView: View { KeymeLottieView(asset: .background, loopMode: .autoReverse) .ignoresSafeArea() - if showBlurringBackground { - BackgroundBlurringView(style: .dark) - .ignoresSafeArea() - .transition(.opacity.animation(Animation.customInteractiveSpring())) - } - SwitchStore(store) { state in + if case .needSignIn = state { + EmptyView() + } else { + BackgroundBlurringView(style: .dark) + .ignoresSafeArea() + .transition(.opacity.animation(Animation.customInteractiveSpring())) + } + switch state { case .needSignIn: CaseLet( @@ -46,9 +48,6 @@ public struct SwitchingRootView: View { } .zIndex(ViewZIndex.siginIn.rawValue) .transition(.opacity.animation(Animation.customInteractiveSpring())) - .onDisappear { - showBlurringBackground = true - } case .needRegistration: CaseLet( From 9197ccf5b81deeeaf3036755d5c98d8353caaaef Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 10:59:09 +0900 Subject: [PATCH 21/30] =?UTF-8?q?refactor=20#89:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/Root/RootFeature+.swift | 44 ----- .../Features/Sources/Root/RootFeature.swift | 90 ++++------ Projects/Features/Sources/Root/RootView.swift | 93 +++++----- .../Sources/Root/SwitchingRootFeature.swift | 165 ------------------ .../Sources/Root/SwitchingRootView.swift | 97 ---------- Projects/Keyme/Sources/KeymeApp.swift | 2 +- 6 files changed, 73 insertions(+), 418 deletions(-) delete mode 100644 Projects/Features/Sources/Root/SwitchingRootFeature.swift delete mode 100644 Projects/Features/Sources/Root/SwitchingRootView.swift diff --git a/Projects/Features/Sources/Root/RootFeature+.swift b/Projects/Features/Sources/Root/RootFeature+.swift index f9d8d004..9ce98bd2 100644 --- a/Projects/Features/Sources/Root/RootFeature+.swift +++ b/Projects/Features/Sources/Root/RootFeature+.swift @@ -61,47 +61,3 @@ extension RootFeature.UserNotificationCenterDelegateManager: MessagingDelegate { tokenSemaphore.signal() } } - -extension SwitchingRootFeature { - final class UserNotificationCenterDelegateManager: NSObject { - private var fcmToken: String? - private let tokenSemaphore = DispatchSemaphore(value: 0) - - func waitForToken() async -> String? { - startRegister() - - return await withCheckedContinuation { continuation in - // If the token has already been received before this method was called - if let token = self.fcmToken { - continuation.resume(returning: token) - return - } - - // Wait for the token to be received - DispatchQueue.global().async { - _ = self.tokenSemaphore.wait(timeout: .now() + 10) - if let token = self.fcmToken { - continuation.resume(returning: token) - } else { - continuation.resume(returning: nil) - } - } - } - } - - private func startRegister() { - Messaging.messaging().delegate = self - } - } -} - -extension SwitchingRootFeature.UserNotificationCenterDelegateManager: MessagingDelegate { - func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - guard let token = fcmToken else { - return - } - - self.fcmToken = token - tokenSemaphore.signal() - } -} diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index f3fb0da6..3d79106f 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -2,14 +2,11 @@ // RootFeature.swift // Features // -// Created by 이영빈 on 2023/08/10. -// Edited by 고도 on 2023/08/14. -// +// Created by 이영빈 on 2023/09/04. // Copyright © 2023 team.humanwave. All rights reserved. // import Foundation - import Domain import Network import ComposableArchitecture @@ -20,21 +17,12 @@ public struct RootFeature: Reducer { public init() {} - public struct State: Equatable { - @PresentationState public var logInStatus: SignInFeature.State? - @PresentationState public var registrationState: RegistrationFeature.State? - @PresentationState public var onboardingState: OnboardingFeature.State? - @PresentationState public var mainPageState: MainPageFeature.State? - - var userStatus: UserStatus = .notDetermined - - public enum UserStatus: Equatable { - case notDetermined - case needSignIn - case needRegistration - case needOnboarding - case canUseApp(userId: Int, nickname: String) - } + public enum State: Equatable { + case notDetermined + case needSignIn(SignInFeature.State) + case needRegistration(RegistrationFeature.State) + case needOnboarding(OnboardingFeature.State) + case canUseApp(MainPageFeature.State) } public enum Action { @@ -43,12 +31,12 @@ public struct RootFeature: Reducer { } case view(View) - case login(PresentationAction) - case registration(PresentationAction) - case onboarding(PresentationAction) - case mainPage(PresentationAction) + case login(SignInFeature.Action) + case registration(RegistrationFeature.Action) + case onboarding(OnboardingFeature.Action) + case mainPage(MainPageFeature.Action) - case updateState(State.UserStatus) + case updateState(State) case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?) } @@ -58,12 +46,12 @@ public struct RootFeature: Reducer { case .view(.checkUserStatus): let accessToken = userStorage.accessToken if accessToken == nil { // 로그 아웃 상태 - return .send(.updateState(.needSignIn)) + return .send(.updateState(.needSignIn(.loggedOut))) } else { // 로그인 상태 network.registerAuthorizationToken(accessToken) return .send(.updateMemberInformation(withMemberData: nil)) } - + case .updateMemberInformation(let receviedMemberData): return .run(priority: .userInitiated) { send in let memberInformation: MemberUpdateDTO.MemberData @@ -90,12 +78,13 @@ public struct RootFeature: Reducer { if let userId = memberInformation.id, let nickname = memberInformation.nickname { if memberInformation.isOnboardingClear != true { - await send(.updateState(.needOnboarding)) + await send(.updateState(.needOnboarding(OnboardingFeature.State()))) } else { - await send(.updateState(.canUseApp(userId: userId, nickname: nickname))) + await send(.updateState( + .canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) } } else { - await send(.updateState(.needRegistration)) + await send(.updateState(.needRegistration(RegistrationFeature.State()))) } Task.detached(priority: .low) { @@ -110,26 +99,12 @@ public struct RootFeature: Reducer { } } - case .updateState(let status): - state.userStatus = status - - switch status { - case .notDetermined: - break - case .needSignIn: - state.logInStatus = .loggedIn - case .needRegistration: - state.registrationState = .init() - case .needOnboarding: - state.onboardingState = .init() - case let .canUseApp(userId, nickname): - state.mainPageState = .init(userId: userId, nickname: nickname) - } - + case .updateState(let receivedState): + state = receivedState return .none // MARK: - Presentation actions - case .login(.presented(.signInWithAppleResponse(let response))): + case .login(.signInWithAppleResponse(let response)): switch response { case .success(let body): let token = body.data.token.accessToken @@ -142,7 +117,7 @@ public struct RootFeature: Reducer { return .none } - case .login(.presented(.signInWithKakaoResponse(let response))): + case .login(.signInWithKakaoResponse(let response)): switch response { case .success(let body): let token = body.data.token.accessToken @@ -150,41 +125,40 @@ public struct RootFeature: Reducer { network.registerAuthorizationToken(token) return .send(.updateMemberInformation(withMemberData: nil)) - + case .failure: return .none } - case .registration(.presented(.finishRegisterResponse(let response))): + case .registration(.finishRegisterResponse(let response)): return .send(.updateMemberInformation(withMemberData: response.data)) - case .onboarding(.presented(.testResult(.closeButtonDidTap))): + case .onboarding(.testResult(.closeButtonDidTap)): guard let userId = userStorage.userId, let nickname = userStorage.nickname else { // 멤버 정보 수신 재시도 // TODO: and show alert. 사실 있을 수 없는 케이스긴 함 return .send(.updateMemberInformation(withMemberData: nil)) } - return .send(.updateState(.canUseApp(userId: userId, nickname: nickname))) + return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) - case .mainPage(.presented(.myPage(.settingViewAction(.logout)))): - print("logout from rootview") - return .none + case .mainPage(.myPage(.settingViewAction(.logout))): + return .send(.updateState(.needSignIn(.loggedOut))) default: return .none } } - .ifLet(\.$logInStatus, action: /Action.login) { + .ifCaseLet(/State.needSignIn, action: /Action.login) { SignInFeature() } - .ifLet(\.$registrationState, action: /Action.registration) { + .ifCaseLet(/State.needRegistration, action: /Action.registration) { RegistrationFeature() } - .ifLet(\.$onboardingState, action: /Action.onboarding) { + .ifCaseLet(/State.needOnboarding, action: /Action.onboarding) { OnboardingFeature() } - .ifLet(\.$mainPageState, action: /Action.mainPage) { + .ifCaseLet(/State.canUseApp, action: /Action.mainPage) { MainPageFeature() } } diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index d9dd462c..6d394e55 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -1,8 +1,8 @@ // // RootView.swift -// Keyme +// Features // -// Created by 이영빈 on 2023/08/09. +// Created by 이영빈 on 2023/09/04. // Copyright © 2023 team.humanwave. All rights reserved. // @@ -12,10 +12,11 @@ import ComposableArchitecture import DSKit public struct RootView: View { + @State private var showBlurringBackground = false private let store: StoreOf public init() { - self.store = Store(initialState: RootFeature.State()) { + self.store = Store(initialState: RootFeature.State.notDetermined) { RootFeature() } @@ -23,71 +24,63 @@ public struct RootView: View { } public var body: some View { - WithViewStore(store, observe: { $0.userStatus }, send: RootFeature.Action.view) { viewStore in - ZStack { - // 애니메이션 부웅.. 부웅.. - KeymeLottieView(asset: .background, loopMode: .autoReverse) - .ignoresSafeArea() - - if viewStore.state != .needSignIn { + ZStack { + // 애니메이션 부웅.. 부웅.. + KeymeLottieView(asset: .background, loopMode: .autoReverse) + .ignoresSafeArea() + + SwitchStore(store) { state in + if case .needSignIn = state { + EmptyView() + } else { BackgroundBlurringView(style: .dark) .ignoresSafeArea() .transition(.opacity.animation(Animation.customInteractiveSpring())) } - switch viewStore.state { + switch state { case .needSignIn: - // 회원가입을 하지 않았거나 로그인을 하지 않은 유저 - let loginStore = store.scope( - state: \.$logInStatus, - action: RootFeature.Action.login) - - IfLetStore(loginStore) { store in + CaseLet( + /RootFeature.State.needSignIn, + action: RootFeature.Action.login + ) { store in SignInView(store: store) } - .transition(.opacity.animation(Animation.customInteractiveSpring())) .zIndex(ViewZIndex.siginIn.rawValue) - - case .needRegistration: - // 개인정보 등록 - let registrationStore = store.scope( - state: \.$registrationState, - action: RootFeature.Action.registration) + .transition(.opacity.animation(Animation.customInteractiveSpring())) - IfLetStore(registrationStore) { store in + case .needRegistration: + CaseLet( + /RootFeature.State.needRegistration, + action: RootFeature.Action.registration + ) { store in RegistrationView(store: store) } - .transition(.opacity.animation(Animation.customInteractiveSpring())) .zIndex(ViewZIndex.registration.rawValue) - + .transition(.opacity.animation(Animation.customInteractiveSpring())) + case .needOnboarding: - // 가입했지만 온보딩을 하지 않고 종료했던 유저 - let onboardingStore = store.scope( - state: \.$onboardingState, - action: RootFeature.Action.onboarding) - - IfLetStore(onboardingStore) { store in + CaseLet( + /RootFeature.State.needOnboarding, + action: RootFeature.Action.onboarding + ) { store in OnboardingView(store: store) } - .transition(.opacity.animation(Animation.customInteractiveSpring())) .zIndex(ViewZIndex.onboarding.rawValue) - + .transition(.opacity.animation(Animation.customInteractiveSpring())) + case .canUseApp: - // 가입했고 온보딩을 진행한 유저 - let mainPageStore = store.scope( - state: \.$mainPageState, - action: RootFeature.Action.mainPage) - - IfLetStore(mainPageStore) { store in + CaseLet( + /RootFeature.State.canUseApp, + action: RootFeature.Action.mainPage + ) { store in KeymeMainView(store: store) - } else: { - Text("에러") } - .transition(.opacity.animation(Animation.customInteractiveSpring())) .zIndex(ViewZIndex.main.rawValue) - - case .notDetermined: - EmptyView() + .transition(.opacity.animation(Animation.customInteractiveSpring())) + + default: + Text("") } } } @@ -102,9 +95,3 @@ private extension RootView { case main = 1 } } - -struct RootView_Previews: PreviewProvider { - static var previews: some View { - RootView() - } -} diff --git a/Projects/Features/Sources/Root/SwitchingRootFeature.swift b/Projects/Features/Sources/Root/SwitchingRootFeature.swift deleted file mode 100644 index 718a3d18..00000000 --- a/Projects/Features/Sources/Root/SwitchingRootFeature.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// SwitchingRootFeature.swift -// Features -// -// Created by 이영빈 on 2023/09/04. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import Foundation -import Domain -import Network -import ComposableArchitecture - -public struct SwitchingRootFeature: Reducer { - @Dependency(\.userStorage) private var userStorage - @Dependency(\.keymeAPIManager) private var network - - public init() {} - - public enum State: Equatable { - case notDetermined - case needSignIn(SignInFeature.State) - case needRegistration(RegistrationFeature.State) - case needOnboarding(OnboardingFeature.State) - case canUseApp(MainPageFeature.State) - } - - public enum Action { - public enum View { - case checkUserStatus - } - case view(View) - - case login(SignInFeature.Action) - case registration(RegistrationFeature.Action) - case onboarding(OnboardingFeature.Action) - case mainPage(MainPageFeature.Action) - - case updateState(State) - case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?) - } - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .view(.checkUserStatus): - let accessToken = userStorage.accessToken - if accessToken == nil { // 로그 아웃 상태 - return .send(.updateState(.needSignIn(.loggedOut))) - } else { // 로그인 상태 - network.registerAuthorizationToken(accessToken) - return .send(.updateMemberInformation(withMemberData: nil)) - } - - case .updateMemberInformation(let receviedMemberData): - return .run(priority: .userInitiated) { send in - let memberInformation: MemberUpdateDTO.MemberData - if let receviedMemberData { - memberInformation = receviedMemberData - } else { - memberInformation = try await network.request( - .member(.fetch), - object: MemberUpdateDTO.self - ).data - } - - userStorage.userId = memberInformation.id - userStorage.nickname = memberInformation.nickname - userStorage.friendCode = memberInformation.friendCode - - if let profileImageURL = URL(string: memberInformation.profileImage) { - userStorage.profileImageURL = profileImageURL - } - - if let profileThumbnailURL = URL(string: memberInformation.profileImage) { - userStorage.profileThumbnailURL = profileThumbnailURL - } - - if let userId = memberInformation.id, let nickname = memberInformation.nickname { - if memberInformation.isOnboardingClear != true { - await send(.updateState(.needOnboarding(OnboardingFeature.State()))) - } else { - await send(.updateState( - .canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) - } - } else { - await send(.updateState(.needRegistration(RegistrationFeature.State()))) - } - - Task.detached(priority: .low) { - let notificationDelegate = UserNotificationCenterDelegateManager() - - guard let token = await notificationDelegate.waitForToken() else { - print("푸시토큰 등록 중 에러 발생") - return - } - - _ = try await network.request(.registerPushToken(.register(token))) - } - } - - case .updateState(let receivedState): - state = receivedState - return .none - - // MARK: - Presentation actions - case .login(.signInWithAppleResponse(let response)): - switch response { - case .success(let body): - let token = body.data.token.accessToken - userStorage.accessToken = token - network.registerAuthorizationToken(token) - - return .send(.updateMemberInformation(withMemberData: nil)) - - case .failure: - return .none - } - - case .login(.signInWithKakaoResponse(let response)): - switch response { - case .success(let body): - let token = body.data.token.accessToken - userStorage.accessToken = token - network.registerAuthorizationToken(token) - - return .send(.updateMemberInformation(withMemberData: nil)) - - case .failure: - return .none - } - - case .registration(.finishRegisterResponse(let response)): - return .send(.updateMemberInformation(withMemberData: response.data)) - - case .onboarding(.testResult(.closeButtonDidTap)): - guard let userId = userStorage.userId, let nickname = userStorage.nickname else { - // 멤버 정보 수신 재시도 - // TODO: and show alert. 사실 있을 수 없는 케이스긴 함 - return .send(.updateMemberInformation(withMemberData: nil)) - } - - return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) - - case .mainPage(.myPage(.settingViewAction(.logout))): - print("logout from rootview") - return .none - default: - return .none - } - } - .ifCaseLet(/State.needSignIn, action: /Action.login) { - SignInFeature() - } - .ifCaseLet(/State.needRegistration, action: /Action.registration) { - RegistrationFeature() - } - .ifCaseLet(/State.needOnboarding, action: /Action.onboarding) { - OnboardingFeature() - } - .ifCaseLet(/State.canUseApp, action: /Action.mainPage) { - MainPageFeature() - } - } -} diff --git a/Projects/Features/Sources/Root/SwitchingRootView.swift b/Projects/Features/Sources/Root/SwitchingRootView.swift deleted file mode 100644 index 328901fe..00000000 --- a/Projects/Features/Sources/Root/SwitchingRootView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// SwitchingRootView.swift -// Features -// -// Created by 이영빈 on 2023/09/04. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import SwiftUI -import Core -import ComposableArchitecture -import DSKit - -public struct SwitchingRootView: View { - @State private var showBlurringBackground = false - private let store: StoreOf - - public init() { - self.store = Store(initialState: SwitchingRootFeature.State.notDetermined) { - SwitchingRootFeature() - } - - store.send(.view(.checkUserStatus)) - } - - public var body: some View { - ZStack { - // 애니메이션 부웅.. 부웅.. - KeymeLottieView(asset: .background, loopMode: .autoReverse) - .ignoresSafeArea() - - SwitchStore(store) { state in - if case .needSignIn = state { - EmptyView() - } else { - BackgroundBlurringView(style: .dark) - .ignoresSafeArea() - .transition(.opacity.animation(Animation.customInteractiveSpring())) - } - - switch state { - case .needSignIn: - CaseLet( - /SwitchingRootFeature.State.needSignIn, - action: SwitchingRootFeature.Action.login - ) { store in - SignInView(store: store) - } - .zIndex(ViewZIndex.siginIn.rawValue) - .transition(.opacity.animation(Animation.customInteractiveSpring())) - - case .needRegistration: - CaseLet( - /SwitchingRootFeature.State.needRegistration, - action: SwitchingRootFeature.Action.registration - ) { store in - RegistrationView(store: store) - } - .zIndex(ViewZIndex.registration.rawValue) - .transition(.opacity.animation(Animation.customInteractiveSpring())) - - case .needOnboarding: - CaseLet( - /SwitchingRootFeature.State.needOnboarding, - action: SwitchingRootFeature.Action.onboarding - ) { store in - OnboardingView(store: store) - } - .zIndex(ViewZIndex.onboarding.rawValue) - .transition(.opacity.animation(Animation.customInteractiveSpring())) - - case .canUseApp: - CaseLet( - /SwitchingRootFeature.State.canUseApp, - action: SwitchingRootFeature.Action.mainPage - ) { store in - KeymeMainView(store: store) - } - .zIndex(ViewZIndex.main.rawValue) - .transition(.opacity.animation(Animation.customInteractiveSpring())) - - default: - Text("") - } - } - } - } -} - -private extension SwitchingRootView { - enum ViewZIndex: CGFloat { - case siginIn = 4 - case registration = 3 - case onboarding = 2 - case main = 1 - } -} diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index 1b80b221..fff8c7f6 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -17,7 +17,7 @@ struct KeymeApp: App { var body: some Scene { WindowGroup { - SwitchingRootView() + RootView() .onOpenURL { url in print(url) if (AuthApi.isKakaoTalkLoginUrl(url)) { From 9563edd46ee12d5cf89e90ff303887900329a610 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 11:23:33 +0900 Subject: [PATCH 22/30] =?UTF-8?q?refactor=20#89:=20=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Features/Sources/MainPage/MainPageFeature.swift | 2 +- Projects/Features/Sources/MyPage/MyPageFeature.swift | 4 ++-- Projects/Features/Sources/MyPage/MyPageView.swift | 2 +- Projects/Features/Sources/Root/RootFeature.swift | 6 +++--- Projects/Features/Sources/Setting/SettingFeature.swift | 5 ++++- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Projects/Features/Sources/MainPage/MainPageFeature.swift b/Projects/Features/Sources/MainPage/MainPageFeature.swift index d0b8d2ab..c4c984e7 100644 --- a/Projects/Features/Sources/MainPage/MainPageFeature.swift +++ b/Projects/Features/Sources/MainPage/MainPageFeature.swift @@ -37,7 +37,7 @@ public struct MainPageFeature: Reducer { Reduce { _, action in switch action { - case .myPage(.settingViewAction(.logout)): + case .myPage(.setting(.logout)): print("logout from mainpage") default: break diff --git a/Projects/Features/Sources/MyPage/MyPageFeature.swift b/Projects/Features/Sources/MyPage/MyPageFeature.swift index 91156e80..8ee03777 100644 --- a/Projects/Features/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Features/Sources/MyPage/MyPageFeature.swift @@ -47,7 +47,7 @@ public struct MyPageFeature: Reducer { case requestCircle(MatchRate) case view(View) case scoreListAction(ScoreListFeature.Action) - case settingViewAction(SettingFeature.Action) + case setting(SettingFeature.Action) public enum View: Equatable { case markViewAsShown @@ -140,7 +140,7 @@ public struct MyPageFeature: Reducer { return .none } } - .ifLet(\.settingViewState, action: /Action.settingViewAction) { + .ifLet(\.settingViewState, action: /Action.setting) { SettingFeature() } } diff --git a/Projects/Features/Sources/MyPage/MyPageView.swift b/Projects/Features/Sources/MyPage/MyPageView.swift index 9a5eafa8..520b6f78 100644 --- a/Projects/Features/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/Sources/MyPage/MyPageView.swift @@ -115,7 +115,7 @@ private extension MyPageView { let store = store.scope( state: \.settingViewState, - action: MyPageFeature.Action.settingViewAction) + action: MyPageFeature.Action.setting) return IfLetStore(store) { store in SettingView(store: store) diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 3d79106f..8a48c5e0 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -103,7 +103,7 @@ public struct RootFeature: Reducer { state = receivedState return .none - // MARK: - Presentation actions + // MARK: - Child actions case .login(.signInWithAppleResponse(let response)): switch response { case .success(let body): @@ -136,13 +136,13 @@ public struct RootFeature: Reducer { case .onboarding(.testResult(.closeButtonDidTap)): guard let userId = userStorage.userId, let nickname = userStorage.nickname else { // 멤버 정보 수신 재시도 - // TODO: and show alert. 사실 있을 수 없는 케이스긴 함 return .send(.updateMemberInformation(withMemberData: nil)) } return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) - case .mainPage(.myPage(.settingViewAction(.logout))): + case .mainPage(.myPage(.setting(.logout))): + userStorage.accessToken = nil return .send(.updateState(.needSignIn(.loggedOut))) default: diff --git a/Projects/Features/Sources/Setting/SettingFeature.swift b/Projects/Features/Sources/Setting/SettingFeature.swift index d0751a86..1af3e8dc 100644 --- a/Projects/Features/Sources/Setting/SettingFeature.swift +++ b/Projects/Features/Sources/Setting/SettingFeature.swift @@ -13,7 +13,9 @@ import Network public struct SettingFeature: Reducer { - public struct State: Equatable {} + public struct State: Equatable { + var isPushNotificationEnabled: Bool = true + } public enum Action: Equatable { case logout @@ -33,6 +35,7 @@ public struct SettingFeature: Reducer { case .withdrawal: // TODO: Call api return .none + case .setPushNotification: return .none } From 88ddca0442032fabdb237825d819cb78f898c7f8 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 13:14:14 +0900 Subject: [PATCH 23/30] =?UTF-8?q?feature=20#89:=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Core/Sources/Example.swift | 1 - .../Sources/NotificationManager.swift | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) delete mode 100644 Projects/Core/Sources/Example.swift create mode 100644 Projects/Features/Sources/NotificationManager.swift diff --git a/Projects/Core/Sources/Example.swift b/Projects/Core/Sources/Example.swift deleted file mode 100644 index fecc4ab4..00000000 --- a/Projects/Core/Sources/Example.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/Projects/Features/Sources/NotificationManager.swift b/Projects/Features/Sources/NotificationManager.swift new file mode 100644 index 00000000..eec5ca01 --- /dev/null +++ b/Projects/Features/Sources/NotificationManager.swift @@ -0,0 +1,125 @@ +// +// NotificationManager.swift +// Core +// +// Created by 이영빈 on 2023/09/04. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import ComposableArchitecture +import Foundation +import SwiftUI + +import FirebaseCore +import FirebaseMessaging + +import UserNotifications +import Network + +public final class NotificationManager: NSObject { + public private(set) var isPushNotificationGranted: Bool = false + private let userNotificationCenter = UNUserNotificationCenter.current() + + private var application: UIApplication? + + private var fcmToken: String? + private let tokenSemaphore = DispatchSemaphore(value: 0) + + func setApplication(_ application: UIApplication) { + self.application = application + } + + /// 쓰레드 블로킹이라 웬만하면 비동기로 처리하세요. 까딱하다 앱 작살남 + func registerPushNotification() async -> String? { + guard let application else { + print("UIApplication 등록하고 쓰세요! (푸시알림 매니저 올림)") + return nil + } + + userNotificationCenter.delegate = self + do { + isPushNotificationGranted = try await userNotificationCenter.requestAuthorization( + options: [.alert, .badge, .sound]) + + guard isPushNotificationGranted else { + return nil + } + + // 푸시토큰 애플 서버에 등록하기 + let settings = await userNotificationCenter.notificationSettings() + guard settings.authorizationStatus == .authorized else { + isPushNotificationGranted = false + return nil + } + + DispatchQueue.main.async { + application.registerForRemoteNotifications() + } + + return await waitForToken() + } catch { + return nil + } + } + + func unregisterPushNotification() { + } + + private func waitForToken() async -> String? { + Messaging.messaging().delegate = self + + return await withCheckedContinuation { continuation in + // If the token has already been received before this method was called + if let token = self.fcmToken { + continuation.resume(returning: token) + return + } + + // Wait for the token to be received + DispatchQueue.global().async { + _ = self.tokenSemaphore.wait(timeout: .now() + 10) + if let token = self.fcmToken { + continuation.resume(returning: token) + } else { + continuation.resume(returning: nil) + } + } + } + } +} + +extension NotificationManager: UNUserNotificationCenterDelegate { + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } + let token = tokenParts.joined() + Messaging.messaging().setAPNSToken(deviceToken, type: .unknown) + + print("firebase Device Token: \(token)") + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("firebase Failed to register for remote notifications: \(error)") + } +} + +extension NotificationManager: MessagingDelegate { + public func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + guard let token = fcmToken else { + return + } + + self.fcmToken = token + tokenSemaphore.signal() + } +} + +extension NotificationManager: DependencyKey { + public static var liveValue = NotificationManager() +} + +extension DependencyValues { + var notificationManager: NotificationManager { + get { self[NotificationManager.self] } + set { self[NotificationManager.self] = newValue } + } +} From d0ae5d6e0cc101832fde262c3ae08405afbdd325 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 14:23:40 +0900 Subject: [PATCH 24/30] =?UTF-8?q?feature=20#89:=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=93=B1=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/NotificationManager.swift | 22 ++++--- .../Features/Sources/Root/RootFeature+.swift | 63 ------------------- .../Features/Sources/Root/RootFeature.swift | 5 +- Projects/Keyme/Sources/KeymeApp.swift | 26 -------- 4 files changed, 17 insertions(+), 99 deletions(-) delete mode 100644 Projects/Features/Sources/Root/RootFeature+.swift diff --git a/Projects/Features/Sources/NotificationManager.swift b/Projects/Features/Sources/NotificationManager.swift index eec5ca01..4d53c2f3 100644 --- a/Projects/Features/Sources/NotificationManager.swift +++ b/Projects/Features/Sources/NotificationManager.swift @@ -52,22 +52,30 @@ public final class NotificationManager: NSObject { return nil } - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - - return await waitForToken() + return await waitForToken(for: application) } catch { return nil } } func unregisterPushNotification() { + guard let application else { + print("UIApplication 등록하고 쓰세요! (푸시알림 매니저 올림)") + return + } + + DispatchQueue.main.async { + application.unregisterForRemoteNotifications() + } } - private func waitForToken() async -> String? { + private func waitForToken(for application: UIApplication) async -> String? { Messaging.messaging().delegate = self - + + DispatchQueue.main.async { + application.registerForRemoteNotifications() + } + return await withCheckedContinuation { continuation in // If the token has already been received before this method was called if let token = self.fcmToken { diff --git a/Projects/Features/Sources/Root/RootFeature+.swift b/Projects/Features/Sources/Root/RootFeature+.swift deleted file mode 100644 index 9ce98bd2..00000000 --- a/Projects/Features/Sources/Root/RootFeature+.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// RootFeature+.swift -// Features -// -// Created by 이영빈 on 2023/08/25. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import Combine -import ComposableArchitecture - -import Foundation -import FirebaseCore -import FirebaseMessaging - -import UserNotifications -import Network - -import SwiftUI - -extension RootFeature { - final class UserNotificationCenterDelegateManager: NSObject { - private var fcmToken: String? - private let tokenSemaphore = DispatchSemaphore(value: 0) - - func waitForToken() async -> String? { - startRegister() - - return await withCheckedContinuation { continuation in - // If the token has already been received before this method was called - if let token = self.fcmToken { - continuation.resume(returning: token) - return - } - - // Wait for the token to be received - DispatchQueue.global().async { - _ = self.tokenSemaphore.wait(timeout: .now() + 10) - if let token = self.fcmToken { - continuation.resume(returning: token) - } else { - continuation.resume(returning: nil) - } - } - } - } - - private func startRegister() { - Messaging.messaging().delegate = self - } - } -} - -extension RootFeature.UserNotificationCenterDelegateManager: MessagingDelegate { - func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - guard let token = fcmToken else { - return - } - - self.fcmToken = token - tokenSemaphore.signal() - } -} diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 8a48c5e0..51c6b329 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -13,6 +13,7 @@ import ComposableArchitecture public struct RootFeature: Reducer { @Dependency(\.userStorage) private var userStorage + @Dependency(\.notificationManager) private var notificationManager @Dependency(\.keymeAPIManager) private var network public init() {} @@ -88,9 +89,7 @@ public struct RootFeature: Reducer { } Task.detached(priority: .low) { - let notificationDelegate = UserNotificationCenterDelegateManager() - - guard let token = await notificationDelegate.waitForToken() else { + guard let token = await notificationManager.registerPushNotification() else { print("푸시토큰 등록 중 에러 발생") return } diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index fff8c7f6..9adc4122 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -38,32 +38,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } FirebaseApp.configure() - - UNUserNotificationCenter.current().delegate = self - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in - guard granted else { return } - - // 푸시토큰 애플 서버에 등록하기 - UNUserNotificationCenter.current().getNotificationSettings { settings in - guard settings.authorizationStatus == .authorized else { return } - - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - } - } return true } - - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } - let token = tokenParts.joined() - Messaging.messaging().setAPNSToken(deviceToken, type: .unknown) - - print("firebase Device Token: \(token)") - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("firebase Failed to register for remote notifications: \(error)") - } } From c5beda8df2ad88df1c18ee46582d099fd3e5c8c7 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Mon, 4 Sep 2023 18:00:53 +0900 Subject: [PATCH 25/30] =?UTF-8?q?refactor=20#89:=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainPage/MainPageFeature.swift | 2 +- .../Sources/MyPage/MyPageFeature.swift | 10 +-- .../Features/Sources/MyPage/MyPageView.swift | 33 +++++----- ...er.swift => PushNotificationManager.swift} | 64 ++++++------------- .../Features/Sources/Root/RootFeature.swift | 25 +++++--- Projects/Features/Sources/Root/RootView.swift | 1 + .../Sources/Setting/SettingFeature.swift | 43 +++++++++++-- .../Sources/Setting/SettingView.swift | 23 ++++--- Projects/Keyme/Sources/KeymeApp.swift | 6 +- 9 files changed, 115 insertions(+), 92 deletions(-) rename Projects/Features/Sources/{NotificationManager.swift => PushNotificationManager.swift} (58%) diff --git a/Projects/Features/Sources/MainPage/MainPageFeature.swift b/Projects/Features/Sources/MainPage/MainPageFeature.swift index c4c984e7..9347c169 100644 --- a/Projects/Features/Sources/MainPage/MainPageFeature.swift +++ b/Projects/Features/Sources/MainPage/MainPageFeature.swift @@ -37,7 +37,7 @@ public struct MainPageFeature: Reducer { Reduce { _, action in switch action { - case .myPage(.setting(.logout)): + case .myPage(.setting(.presented(.view(.logout)))): print("logout from mainpage") default: break diff --git a/Projects/Features/Sources/MyPage/MyPageFeature.swift b/Projects/Features/Sources/MyPage/MyPageFeature.swift index 8ee03777..e31051fc 100644 --- a/Projects/Features/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Features/Sources/MyPage/MyPageFeature.swift @@ -22,7 +22,7 @@ public struct MyPageFeature: Reducer { var differentCircleDataList: [CircleData] = [] var view: View @Box var scoreListState: ScoreListFeature.State - @Box var settingViewState: SettingFeature.State? + @PresentationState var settingViewState: SettingFeature.State? struct View: Equatable { let userId: Int @@ -37,7 +37,6 @@ public struct MyPageFeature: Reducer { init(userId: Int, nickname: String) { self.view = View(userId: userId, nickname: nickname) self._scoreListState = .init(.init()) - self._settingViewState = .init(nil) } } @@ -47,7 +46,7 @@ public struct MyPageFeature: Reducer { case requestCircle(MatchRate) case view(View) case scoreListAction(ScoreListFeature.Action) - case setting(SettingFeature.Action) + case setting(PresentationAction) public enum View: Equatable { case markViewAsShown @@ -129,7 +128,8 @@ public struct MyPageFeature: Reducer { return .none case .view(.prepareSettingView): - state.settingViewState = .init() + print("@@ init from mypage") + state.settingViewState = SettingFeature.State() return .none case .scoreListAction: @@ -140,7 +140,7 @@ public struct MyPageFeature: Reducer { return .none } } - .ifLet(\.settingViewState, action: /Action.setting) { + .ifLet(\.$settingViewState, action: /Action.setting) { SettingFeature() } } diff --git a/Projects/Features/Sources/MyPage/MyPageView.swift b/Projects/Features/Sources/MyPage/MyPageView.swift index 520b6f78..d7bc257f 100644 --- a/Projects/Features/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/Sources/MyPage/MyPageView.swift @@ -53,12 +53,12 @@ struct MyPageView: View { if !viewStore.state.circleShown { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { - NavigationLink(destination: destinationView()) { + Button(action: { viewStore.send(.prepareSettingView) }) { DSKitAsset.Image.photoExport.swiftUIImage .resizable() .frame(width: 35, height: 35) } - + Spacer() Text.keyme("마이", font: .body3Semibold) @@ -69,7 +69,7 @@ struct MyPageView: View { Spacer() - NavigationLink(destination: destinationView()) { + Button(action: { viewStore.send(.prepareSettingView) }) { DSKitAsset.Image.setting.swiftUIImage .resizable() .frame(width: 24, height: 24) @@ -99,6 +99,9 @@ struct MyPageView: View { .foregroundColor(.white) } } + .navigationDestination( + store: store.scope(state: \.$settingViewState, action: MyPageFeature.Action.setting), + destination: { SettingView(store: $0) }) } .onAppear { store.send(.requestCircle(.top5)) @@ -109,16 +112,14 @@ struct MyPageView: View { } } -private extension MyPageView { - func destinationView() -> some View { - store.send(.view(.prepareSettingView)) - - let store = store.scope( - state: \.settingViewState, - action: MyPageFeature.Action.setting) - - return IfLetStore(store) { store in - SettingView(store: store) - } - } -} +//private extension MyPageView { +// func destinationView() -> some View { +// let store = store.scope( +// state: \.settingViewState, +// action: MyPageFeature.Action.setting) +// +// return IfLetStore(store) { store in +// SettingView(store: store) +// } +// } +//} diff --git a/Projects/Features/Sources/NotificationManager.swift b/Projects/Features/Sources/PushNotificationManager.swift similarity index 58% rename from Projects/Features/Sources/NotificationManager.swift rename to Projects/Features/Sources/PushNotificationManager.swift index 4d53c2f3..f8cefd46 100644 --- a/Projects/Features/Sources/NotificationManager.swift +++ b/Projects/Features/Sources/PushNotificationManager.swift @@ -1,6 +1,6 @@ // -// NotificationManager.swift -// Core +// PushNotificationManager.swift +// Feature // // Created by 이영빈 on 2023/09/04. // Copyright © 2023 team.humanwave. All rights reserved. @@ -16,27 +16,19 @@ import FirebaseMessaging import UserNotifications import Network -public final class NotificationManager: NSObject { +public final class PushNotificationManager: NSObject { public private(set) var isPushNotificationGranted: Bool = false private let userNotificationCenter = UNUserNotificationCenter.current() - - private var application: UIApplication? + private var application: UIApplication = .shared private var fcmToken: String? private let tokenSemaphore = DispatchSemaphore(value: 0) - func setApplication(_ application: UIApplication) { - self.application = application - } - /// 쓰레드 블로킹이라 웬만하면 비동기로 처리하세요. 까딱하다 앱 작살남 - func registerPushNotification() async -> String? { - guard let application else { - print("UIApplication 등록하고 쓰세요! (푸시알림 매니저 올림)") - return nil - } - + public func registerPushNotification() async -> String? { userNotificationCenter.delegate = self + Messaging.messaging().delegate = self + do { isPushNotificationGranted = try await userNotificationCenter.requestAuthorization( options: [.alert, .badge, .sound]) @@ -58,23 +50,17 @@ public final class NotificationManager: NSObject { } } - func unregisterPushNotification() { - guard let application else { - print("UIApplication 등록하고 쓰세요! (푸시알림 매니저 올림)") - return - } - + public func unregisterPushNotification() { DispatchQueue.main.async { - application.unregisterForRemoteNotifications() + self.application.unregisterForRemoteNotifications() } } private func waitForToken(for application: UIApplication) async -> String? { - Messaging.messaging().delegate = self - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } +// DispatchQueue.main.async { + await application.registerForRemoteNotifications() +// } return await withCheckedContinuation { continuation in // If the token has already been received before this method was called @@ -96,21 +82,9 @@ public final class NotificationManager: NSObject { } } -extension NotificationManager: UNUserNotificationCenterDelegate { - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } - let token = tokenParts.joined() - Messaging.messaging().setAPNSToken(deviceToken, type: .unknown) - - print("firebase Device Token: \(token)") - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - print("firebase Failed to register for remote notifications: \(error)") - } -} +extension PushNotificationManager: UNUserNotificationCenterDelegate {} -extension NotificationManager: MessagingDelegate { +extension PushNotificationManager: MessagingDelegate { public func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { guard let token = fcmToken else { return @@ -121,13 +95,13 @@ extension NotificationManager: MessagingDelegate { } } -extension NotificationManager: DependencyKey { - public static var liveValue = NotificationManager() +extension PushNotificationManager: DependencyKey { + public static var liveValue = PushNotificationManager() } extension DependencyValues { - var notificationManager: NotificationManager { - get { self[NotificationManager.self] } - set { self[NotificationManager.self] = newValue } + public var notificationManager: PushNotificationManager { + get { self[PushNotificationManager.self] } + set { self[PushNotificationManager.self] = newValue } } } diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 51c6b329..791217da 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -10,6 +10,7 @@ import Foundation import Domain import Network import ComposableArchitecture +import SwiftUI public struct RootFeature: Reducer { @Dependency(\.userStorage) private var userStorage @@ -39,6 +40,7 @@ public struct RootFeature: Reducer { case updateState(State) case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?) + case registerPushNotification } public var body: some ReducerOf { @@ -88,20 +90,25 @@ public struct RootFeature: Reducer { await send(.updateState(.needRegistration(RegistrationFeature.State()))) } - Task.detached(priority: .low) { - guard let token = await notificationManager.registerPushNotification() else { - print("푸시토큰 등록 중 에러 발생") - return - } - - _ = try await network.request(.registerPushToken(.register(token))) - } + await send(.registerPushNotification) } case .updateState(let receivedState): state = receivedState return .none + case .registerPushNotification: + Task { + guard let token = await notificationManager.registerPushNotification() else { + print("푸시토큰 등록 중 에러 발생") + return + } + + _ = try await network.request(.registerPushToken(.register(token))) + } + + return .none + // MARK: - Child actions case .login(.signInWithAppleResponse(let response)): switch response { @@ -140,7 +147,7 @@ public struct RootFeature: Reducer { return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) - case .mainPage(.myPage(.setting(.logout))): + case .mainPage(.myPage(.setting(.presented(.view(.logout))))): userStorage.accessToken = nil return .send(.updateState(.needSignIn(.loggedOut))) diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index 6d394e55..ab64ad6e 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -30,6 +30,7 @@ public struct RootView: View { .ignoresSafeArea() SwitchStore(store) { state in + // 블러 깔 것인지 판단(로그인 아니면 깐다) if case .needSignIn = state { EmptyView() } else { diff --git a/Projects/Features/Sources/Setting/SettingFeature.swift b/Projects/Features/Sources/Setting/SettingFeature.swift index 1af3e8dc..aa3e0c10 100644 --- a/Projects/Features/Sources/Setting/SettingFeature.swift +++ b/Projects/Features/Sources/Setting/SettingFeature.swift @@ -12,15 +12,27 @@ import Domain import Network public struct SettingFeature: Reducer { + @Dependency(\.notificationManager) var notificationManager public struct State: Equatable { - var isPushNotificationEnabled: Bool = true + var isPushNotificationEnabled: Bool + + init() { + @Dependency(\.notificationManager.isPushNotificationGranted) var isPushNotificationGranted + self.isPushNotificationEnabled = isPushNotificationGranted + print("@@ init") + } } public enum Action: Equatable { - case logout - case withdrawal - case setPushNotification + public enum View: Equatable { + case logout + case withdrawal + case togglePushNotification + } + + case view(View) + case setPushNotificationStatus(Bool) } public init() { } @@ -28,15 +40,32 @@ public struct SettingFeature: Reducer { public var body: some Reducer { Reduce { state, action in switch action { - case .logout: + case .view(.logout): print("logout from setting") return .none - case .withdrawal: + case .view(.withdrawal): // TODO: Call api return .none - case .setPushNotification: + case .view(.togglePushNotification): + if state.isPushNotificationEnabled == false { + // 푸시알림 설정 + return .run { send in + guard await notificationManager.registerPushNotification() != nil else { + return + } + await send(.setPushNotificationStatus(true)) + } + } else { + // 푸시알림 해제 + notificationManager.unregisterPushNotification() + return .send(.setPushNotificationStatus(false)) + } + + case .setPushNotificationStatus(let value): + state.isPushNotificationEnabled = value + print("@@", value) return .none } } diff --git a/Projects/Features/Sources/Setting/SettingView.swift b/Projects/Features/Sources/Setting/SettingView.swift index ff2abfcf..34a1ec0b 100644 --- a/Projects/Features/Sources/Setting/SettingView.swift +++ b/Projects/Features/Sources/Setting/SettingView.swift @@ -22,7 +22,7 @@ struct SettingView: View { } var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(store, observe: { $0 }, send: SettingFeature.Action.view) { viewStore in ZStack { DSKitAsset.Color.keymeBlack.swiftUIColor .ignoresSafeArea() @@ -37,13 +37,10 @@ struct SettingView: View { Divider() section(title: "마케팅 정보 수신 동의") { - HStack { - item(text: "푸시 알림") - - Spacer() - - Toggle("", isOn: .constant(true)) - } + pushNotificationToggleButton( + isOn: viewStore.binding( + get: \.isPushNotificationEnabled, + send: .togglePushNotification)) } } .fullFrame() @@ -90,6 +87,16 @@ private extension SettingView { } } + func pushNotificationToggleButton(isOn binding: Binding) -> some View { + HStack { + item(text: "푸시 알림") + + Spacer() + + Toggle("", isOn: binding) + } + } + func section(title: String, @ViewBuilder items: () -> some View) -> some View { VStack(alignment: .leading, spacing: 0) { caption(text: title) diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index 9adc4122..2ebaded6 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -29,6 +29,8 @@ struct KeymeApp: App { } class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { + @Dependency(\.notificationManager) var notificationManager + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -36,8 +38,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if let kakaoAPIKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_API_KEY") as? String { KakaoSDK.initSDK(appKey: kakaoAPIKey) } - FirebaseApp.configure() + + Task { await notificationManager.registerPushNotification() } + return true } } From f133d2b15aa467d33b3a6a97ce82939a17dbd606 Mon Sep 17 00:00:00 2001 From: enebin Date: Tue, 5 Sep 2023 09:13:38 +0900 Subject: [PATCH 26/30] =?UTF-8?q?refactor=20#89:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Sources/MyPage/MyPageView.swift | 12 --------- .../Sources/PushNotificationManager.swift | 5 +--- .../Features/Sources/Root/RootFeature.swift | 6 ++--- .../Sources/Setting/SettingView.swift | 2 +- .../Sources/SignIn/SignInFeature.swift | 27 +++++++++++++------ .../Features/Sources/SignIn/SignInView.swift | 6 +++++ 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Projects/Features/Sources/MyPage/MyPageView.swift b/Projects/Features/Sources/MyPage/MyPageView.swift index d7bc257f..d1441192 100644 --- a/Projects/Features/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/Sources/MyPage/MyPageView.swift @@ -111,15 +111,3 @@ struct MyPageView: View { } } } - -//private extension MyPageView { -// func destinationView() -> some View { -// let store = store.scope( -// state: \.settingViewState, -// action: MyPageFeature.Action.setting) -// -// return IfLetStore(store) { store in -// SettingView(store: store) -// } -// } -//} diff --git a/Projects/Features/Sources/PushNotificationManager.swift b/Projects/Features/Sources/PushNotificationManager.swift index f8cefd46..702036c0 100644 --- a/Projects/Features/Sources/PushNotificationManager.swift +++ b/Projects/Features/Sources/PushNotificationManager.swift @@ -57,10 +57,7 @@ public final class PushNotificationManager: NSObject { } private func waitForToken(for application: UIApplication) async -> String? { - -// DispatchQueue.main.async { - await application.registerForRemoteNotifications() -// } + await application.registerForRemoteNotifications() return await withCheckedContinuation { continuation in // If the token has already been received before this method was called diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 791217da..01d87820 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -49,7 +49,7 @@ public struct RootFeature: Reducer { case .view(.checkUserStatus): let accessToken = userStorage.accessToken if accessToken == nil { // 로그 아웃 상태 - return .send(.updateState(.needSignIn(.loggedOut))) + return .send(.updateState(.needSignIn(SignInFeature.State()))) } else { // 로그인 상태 network.registerAuthorizationToken(accessToken) return .send(.updateMemberInformation(withMemberData: nil)) @@ -148,8 +148,8 @@ public struct RootFeature: Reducer { return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) case .mainPage(.myPage(.setting(.presented(.view(.logout))))): - userStorage.accessToken = nil - return .send(.updateState(.needSignIn(.loggedOut))) +] userStorage.accessToken = nil + return .send(.updateState(.needSignIn(SignInFeature.State()))) default: return .none diff --git a/Projects/Features/Sources/Setting/SettingView.swift b/Projects/Features/Sources/Setting/SettingView.swift index 34a1ec0b..ad54a479 100644 --- a/Projects/Features/Sources/Setting/SettingView.swift +++ b/Projects/Features/Sources/Setting/SettingView.swift @@ -26,7 +26,7 @@ struct SettingView: View { ZStack { DSKitAsset.Color.keymeBlack.swiftUIColor .ignoresSafeArea() - + ScrollView { VStack(alignment: .leading, spacing: 30) { section(title: "개인정보") { diff --git a/Projects/Features/Sources/SignIn/SignInFeature.swift b/Projects/Features/Sources/SignIn/SignInFeature.swift index ccfa0b08..b87a8a9b 100644 --- a/Projects/Features/Sources/SignIn/SignInFeature.swift +++ b/Projects/Features/Sources/SignIn/SignInFeature.swift @@ -24,10 +24,15 @@ public enum SignInError: Error { public struct SignInFeature: Reducer { @Dependency(\.keymeAPIManager) var network - public enum State: Equatable { - case notDetermined - case loggedIn - case loggedOut + public struct State: Equatable { + var isLoading: Bool = false + var status: Status = .notDetermined + + enum Status { + case notDetermined + case loggedIn + case loggedOut + } } public enum Action: Equatable { @@ -43,20 +48,26 @@ public struct SignInFeature: Reducer { switch action { // MARK: - Kakao case .signInWithKakao: + state.isLoading = true + return .run { send in await send(.signInWithKakaoResponse(TaskResult { try await signInWithKakao() })) } case .signInWithKakaoResponse(.success): // 카카오 로그인 성공 - state = .loggedIn + state.isLoading = false + state.status = .loggedIn return .none case .signInWithKakaoResponse(.failure): // 카카오 로그인 실패 - state = .loggedOut + state.isLoading = false + state.status = .loggedOut return .none // MARK: - Apple case .signInWithApple(let authorization): + state.isLoading = true + guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, let identityTokenData = appleIDCredential.identityToken, @@ -72,11 +83,11 @@ public struct SignInFeature: Reducer { } case .signInWithAppleResponse(.success): // 애플 로그인 성공 - state = .loggedIn + state.status = .loggedIn return .none case .signInWithAppleResponse(.failure): // 애플 로그인 실패 - state = .loggedOut + state.status = .loggedOut return .none } } diff --git a/Projects/Features/Sources/SignIn/SignInView.swift b/Projects/Features/Sources/SignIn/SignInView.swift index 9ba1d816..7f70e138 100644 --- a/Projects/Features/Sources/SignIn/SignInView.swift +++ b/Projects/Features/Sources/SignIn/SignInView.swift @@ -23,6 +23,12 @@ public struct SignInView: View { } public var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + if viewStore.state.isLoading { + ProgressView() + } + } + ZStack(alignment: .center) { Text.keyme("KEYME", font: .checkResult) .foregroundColor(.white) From ea19516fd3f764d08fb11ff581a4c7edfcca61c4 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Tue, 5 Sep 2023 10:51:15 +0900 Subject: [PATCH 27/30] =?UTF-8?q?refactor=20#89:=20=EC=9B=B9=20=EB=B7=B0?= =?UTF-8?q?=20=EB=A1=9C=EB=94=A9=EC=86=8D=EB=8F=84=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utility/UI/WebView/WebViewWarmUper.swift | 68 +++++++++++++++++++ .../KeymeTests/WebView/KeymeWebView.swift | 3 +- .../Features/Sources/Root/RootFeature.swift | 2 +- Projects/Keyme/Sources/KeymeApp.swift | 4 ++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 Projects/Core/Sources/Utility/UI/WebView/WebViewWarmUper.swift diff --git a/Projects/Core/Sources/Utility/UI/WebView/WebViewWarmUper.swift b/Projects/Core/Sources/Utility/UI/WebView/WebViewWarmUper.swift new file mode 100644 index 00000000..e794909f --- /dev/null +++ b/Projects/Core/Sources/Utility/UI/WebView/WebViewWarmUper.swift @@ -0,0 +1,68 @@ +// https://github.com/bernikovich/WebViewWarmUper +// +// Created by Timur Bernikovich on 07/03/2019. +// Copyright © 2019 Timur Bernikovich. All rights reserved. +// + +import WebKit +import UIKit + +public protocol WarmUpable { + func warmUp() +} + +public class WarmUper { + + private let creationClosure: () -> Object + private var warmedUpObjects: [Object] = [] + public var numberOfWamedUpObjects: Int = 2 { + didSet { + prepare() + } + } + + public init(creationClosure: @escaping () -> Object) { + self.creationClosure = creationClosure + prepare() + } + + public func prepare() { + while warmedUpObjects.count < numberOfWamedUpObjects { + let object = creationClosure() + object.warmUp() + warmedUpObjects.append(object) + } + } + + private func createObjectAndWarmUp() -> Object { + let object = creationClosure() + object.warmUp() + return object + } + + public func dequeue() -> Object { + let warmedUpObject: Object + if let object = warmedUpObjects.first { + warmedUpObjects.removeFirst() + warmedUpObject = object + } else { + warmedUpObject = createObjectAndWarmUp() + } + prepare() + return warmedUpObject + } + +} + +extension WKWebView: WarmUpable { + public func warmUp() { + loadHTMLString("", baseURL: nil) + } +} + +public typealias WKWebViewWarmUper = WarmUper +public extension WarmUper where Object == WKWebView { + static let shared = WKWebViewWarmUper(creationClosure: { + WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + }) +} diff --git a/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift b/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift index ff4d60e7..b0cbc1dd 100644 --- a/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift +++ b/Projects/Features/Sources/KeymeTests/WebView/KeymeWebView.swift @@ -9,6 +9,7 @@ import SwiftUI import WebKit +import Core import Domain import DSKit @@ -33,7 +34,7 @@ public struct KeymeWebView: UIViewRepresentable { init(url: String, accessToken: String) { self.option = .init() self.url = url - self.webView = WKWebView(frame: CGRect.zero, configuration: WKWebViewConfiguration()) + self.webView = WKWebViewWarmUper.shared.dequeue() if let encodedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 01d87820..89bac0a0 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -148,7 +148,7 @@ public struct RootFeature: Reducer { return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) case .mainPage(.myPage(.setting(.presented(.view(.logout))))): -] userStorage.accessToken = nil + userStorage.accessToken = nil return .send(.updateState(.needSignIn(SignInFeature.State()))) default: diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift index 2ebaded6..5adfa321 100644 --- a/Projects/Keyme/Sources/KeymeApp.swift +++ b/Projects/Keyme/Sources/KeymeApp.swift @@ -1,6 +1,7 @@ import SwiftUI import UserNotifications +import Core import ComposableArchitecture import FirebaseCore import FirebaseMessaging @@ -41,6 +42,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD FirebaseApp.configure() Task { await notificationManager.registerPushNotification() } + + // 웹 뷰 로딩속도 개선 툴 + WKWebViewWarmUper.shared.prepare() return true } From 6f151147d72fbd0e8c7dbba615dc94c243cc9b5d Mon Sep 17 00:00:00 2001 From: enebin Date: Wed, 6 Sep 2023 09:10:09 +0900 Subject: [PATCH 28/30] =?UTF-8?q?refactor=20#89:=20=ED=99=88=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/KeymeTestsStartFeature.swift | 13 +++++---- .../Sources/Home/KeymeTestsStartView.swift | 28 ++++++++----------- .../KeymeTests/KeymeTestsFeature.swift | 13 +++++++++ .../Sources/KeymeTests/KeymeTestsView.swift | 13 +++++++-- .../Onboarding/OnboardingFeature.swift | 3 ++ 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift index 670a88cd..91918f80 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift @@ -17,7 +17,7 @@ public struct KeymeTestsStartFeature: Reducer { public let testData: KeymeTestsModel public var icon: IconModel = .EMPTY - public var keymeTests: KeymeTestsFeature.State? + @PresentationState public var keymeTests: KeymeTestsFeature.State? public var isAnimating: Bool = false public init(nickname: String, testData: KeymeTestsModel) { @@ -31,7 +31,7 @@ public struct KeymeTestsStartFeature: Reducer { case startAnimation([IconModel]) case setIcon(IconModel) case startButtonDidTap - case keymeTests(KeymeTestsFeature.Action) + case keymeTests(PresentationAction) } @Dependency(\.continuousClock) var clock @@ -67,13 +67,16 @@ public struct KeymeTestsStartFeature: Reducer { let url = "https://keyme-frontend.vercel.app/test/\(state.testData.testId)" state.keymeTests = KeymeTestsFeature.State(url: url) - case .keymeTests: - return .none + case .keymeTests(.presented(.close)): + state.keymeTests = nil + + default: + break } return .none } - .ifLet(\.keymeTests, action: /Action.keymeTests) { + .ifLet(\.$keymeTests, action: /Action.keymeTests) { KeymeTestsFeature() } } diff --git a/Projects/Features/Sources/Home/KeymeTestsStartView.swift b/Projects/Features/Sources/Home/KeymeTestsStartView.swift index ded1d02f..1276df28 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartView.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartView.swift @@ -23,23 +23,19 @@ public struct KeymeTestsStartView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - IfLetStore( - self.store.scope( - state: \.keymeTests, - action: KeymeTestsStartFeature.Action.keymeTests - ), - then: { store in - KeymeTestsView(store: store) - .ignoresSafeArea(.all) - .transition(.scale.animation(.easeIn)) - }, - else: { - startTestsButton(viewStore) - .onTapGesture { - viewStore.send(.startButtonDidTap) - } + startTestsButton(viewStore) + .onTapGesture { + viewStore.send(.startButtonDidTap) } - ) + .navigationDestination( + store: store.scope( + state: \.$keymeTests, + action: KeymeTestsStartFeature.Action.keymeTests + ), destination: { store in + KeymeTestsView(store: store) + .ignoresSafeArea(.all) + .transition(.scale.animation(.easeIn)) + }) } .onAppear { store.send(.viewWillAppear) diff --git a/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift b/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift index 17e3eeb3..14f2c39c 100644 --- a/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift +++ b/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift @@ -20,6 +20,8 @@ public struct KeymeTestsFeature: Reducer { return authorizationToken } + @PresentationState var alertState: AlertState? + public init(url: String) { self.url = url } @@ -28,9 +30,13 @@ public struct KeymeTestsFeature: Reducer { public enum Action: Equatable { case transition case close + case closeButtonTapped case submit(resultCode: String, testResultId: Int) case showResult(data: KeymeWebViewModel) case postResult(TaskResult) + case alert(PresentationAction) + + public enum Alert: Equatable {} } @Dependency(\.keymeTestsClient) var keymeTestsClient @@ -42,10 +48,13 @@ public struct KeymeTestsFeature: Reducer { switch action { case .transition: return .none + case .close: return .none + case .submit(let code, let id): return .none + case .showResult(let data): return .run { [resultCode = data.resultCode] send in await send(.postResult( @@ -54,6 +63,10 @@ public struct KeymeTestsFeature: Reducer { } )) } + + case .alert: + return .none + default: return .none } diff --git a/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift b/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift index 9ea1ea96..78cb6d4e 100644 --- a/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift +++ b/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift @@ -13,6 +13,8 @@ import ComposableArchitecture import DSKit public struct KeymeTestsView: View { + @State private var showCloseAlert = false + let store: StoreOf public init(store: StoreOf) { @@ -22,15 +24,22 @@ public struct KeymeTestsView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in ZStack { - KeymeWebView(url: viewStore.url, accessToken: viewStore.authorizationToken ?? "") // TODO: handle it + KeymeWebView(url: viewStore.url, accessToken: viewStore.authorizationToken ?? "") // TODO: handle it .onCloseWebView { - print("close") + showCloseAlert = true } .onTestSubmitted { testResult in viewStore.send(.showResult(data: testResult)) } + .toolbar(.hidden, for: .navigationBar) } .frame(maxWidth: .infinity, maxHeight: .infinity) + .alert("", isPresented: $showCloseAlert) { + Button("취소", role: .cancel) { } + Button("종료") { viewStore.send(.close) } + } message: { + Text("테스트를 종료하시겠어요?") + } } } } diff --git a/Projects/Features/Sources/Onboarding/OnboardingFeature.swift b/Projects/Features/Sources/Onboarding/OnboardingFeature.swift index 98fd5893..67c411e4 100644 --- a/Projects/Features/Sources/Onboarding/OnboardingFeature.swift +++ b/Projects/Features/Sources/Onboarding/OnboardingFeature.swift @@ -140,6 +140,9 @@ public struct OnboardingFeature: Reducer { case .testResult(.closeButtonDidTap): state.status = .completed + case .keymeTests(.presented(.close)): + state.keymeTestsState = nil + default: break } From ae04e23a9209d6c4d78c2f82747342e15d855f88 Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Wed, 6 Sep 2023 10:30:57 +0900 Subject: [PATCH 29/30] =?UTF-8?q?feature=20#89:=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20`alert`=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/KeymeTestHomeFeature.swift | 53 ++++++++++++++++++- .../Sources/Home/KeymeTestHomeView.swift | 1 + .../Sources/Home/KeymeTestsStartFeature.swift | 6 ++- .../KeymeTests/KeymeTestsFeature.swift | 53 ++++++++++++++----- .../Sources/KeymeTests/KeymeTestsView.swift | 14 ++--- .../Sources/MainPage/MainPageFeature.swift | 8 +-- .../Onboarding/OnboardingFeature.swift | 13 ++--- .../Features/Sources/Root/RootFeature.swift | 52 +++++++++++++----- 8 files changed, 146 insertions(+), 54 deletions(-) diff --git a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift index 74ed6259..08e6a2c0 100644 --- a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift @@ -9,13 +9,19 @@ import ComposableArchitecture import Domain import Network +import Foundation public struct KeymeTestsHomeFeature: Reducer { @Dependency(\.keymeAPIManager) private var network // 테스트를 아직 풀지 않았거나, 풀었거나 2가지 케이스만 존재 public struct State: Equatable { + @PresentationState var alertState: AlertState? @PresentationState var testStartViewState: KeymeTestsStartFeature.State? + var authorizationToken: String? { + @Dependency(\.keymeAPIManager.authorizationToken) var authorizationToken + return authorizationToken + } var view: View struct View: Equatable { @@ -29,11 +35,31 @@ public struct KeymeTestsHomeFeature: Reducer { } public enum Action { + case requestLogout + case fetchDailyTests case showTestStartView(testData: KeymeTestsModel) + case showErrorAlert(HomeFeatureError) + + case alert(PresentationAction) case startTest(PresentationAction) enum View {} + + public enum Alert: Equatable { + case error(HomeFeatureError) + } + + public enum HomeFeatureError: LocalizedError { + case cannotGetAuthorizationInformation + + public var errorDescription: String? { + switch self { + case .cannotGetAuthorizationInformation: + return "로그인 정보를 불러올 수 없습니다. 다시 로그인을 진행해주세요." + } + } + } } public var body: some ReducerOf { @@ -49,7 +75,32 @@ public struct KeymeTestsHomeFeature: Reducer { case .showTestStartView(let testData): state.view.dailyTestId = testData.testId - state.testStartViewState = .init(nickname: state.view.nickname, testData: testData) + guard let authorizationToken = state.authorizationToken else { + return .send(.showErrorAlert(.cannotGetAuthorizationInformation)) + } + + state.testStartViewState = KeymeTestsStartFeature.State( + nickname: state.view.nickname, + testData: testData, + authorizationToken: authorizationToken) + + case .showErrorAlert(let error): + if case .cannotGetAuthorizationInformation = error { + state.alertState = AlertState( + title: { TextState("에러 발생") }, + actions: { ButtonState( + action: .error(.cannotGetAuthorizationInformation), + label: { TextState("닫기") } + ) }, + message: { TextState(error.localizedDescription) }) + } + return .none + + case .alert(.presented(.error(let error))): + if case .cannotGetAuthorizationInformation = error { + return .send(.requestLogout) + } + return .none default: break diff --git a/Projects/Features/Sources/Home/KeymeTestHomeView.swift b/Projects/Features/Sources/Home/KeymeTestHomeView.swift index 4ba42556..6f5db17c 100644 --- a/Projects/Features/Sources/Home/KeymeTestHomeView.swift +++ b/Projects/Features/Sources/Home/KeymeTestHomeView.swift @@ -46,6 +46,7 @@ struct KeymeTestsHomeView: View { } } } + .alert(store: store.scope(state: \.$alertState, action: KeymeTestsHomeFeature.Action.alert)) } } diff --git a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift index 91918f80..86bae609 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift @@ -15,14 +15,16 @@ public struct KeymeTestsStartFeature: Reducer { public struct State: Equatable { public let nickname: String public let testData: KeymeTestsModel + let authorizationToken: String public var icon: IconModel = .EMPTY @PresentationState public var keymeTests: KeymeTestsFeature.State? public var isAnimating: Bool = false - public init(nickname: String, testData: KeymeTestsModel) { + public init(nickname: String, testData: KeymeTestsModel, authorizationToken: String) { self.nickname = nickname self.testData = testData + self.authorizationToken = authorizationToken } } @@ -65,7 +67,7 @@ public struct KeymeTestsStartFeature: Reducer { case .startButtonDidTap: let url = "https://keyme-frontend.vercel.app/test/\(state.testData.testId)" - state.keymeTests = KeymeTestsFeature.State(url: url) + state.keymeTests = KeymeTestsFeature.State(url: url, authorizationToken: state.authorizationToken) case .keymeTests(.presented(.close)): state.keymeTests = nil diff --git a/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift b/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift index 14f2c39c..0f916aab 100644 --- a/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift +++ b/Projects/Features/Sources/KeymeTests/KeymeTestsFeature.swift @@ -7,6 +7,7 @@ // import ComposableArchitecture +import Foundation import Domain import Network @@ -15,28 +16,34 @@ public struct KeymeTestsFeature: Reducer { public struct State: Equatable { let url: String - var authorizationToken: String? { - @Dependency(\.keymeAPIManager.authorizationToken) var authorizationToken - return authorizationToken - } - + let authorizationToken: String @PresentationState var alertState: AlertState? - public init(url: String) { + public init(url: String, authorizationToken: String) { self.url = url + self.authorizationToken = authorizationToken + self.alertState = alertState } } public enum Action: Equatable { case transition case close - case closeButtonTapped + case submit(resultCode: String, testResultId: Int) - case showResult(data: KeymeWebViewModel) case postResult(TaskResult) + + case view(View) case alert(PresentationAction) - public enum Alert: Equatable {} + public enum View: Equatable { + case showResult(data: KeymeWebViewModel) + case closeButtonTapped + } + + public enum Alert: Equatable { + case closeTest + } } @Dependency(\.keymeTestsClient) var keymeTestsClient @@ -52,10 +59,23 @@ public struct KeymeTestsFeature: Reducer { case .close: return .none - case .submit(let code, let id): - return .none + // MARK: - View actions + case .view(.closeButtonTapped): + state.alertState = AlertState( + title: { TextState("") }, + actions: { + ButtonState( + role: .cancel, + label: { TextState("취소") } + ) + ButtonState( + action: .closeTest, + label: { TextState("종료") } + ) + }, + message: { TextState("테스트를 종료하시겠어요?") }) - case .showResult(let data): + case .view(.showResult(let data)): return .run { [resultCode = data.resultCode] send in await send(.postResult( TaskResult { try await @@ -63,13 +83,18 @@ public struct KeymeTestsFeature: Reducer { } )) } - - case .alert: + + case .submit: return .none + case .alert(.presented(.closeTest)): + return .send(.close) + default: return .none } + + return .none } } } diff --git a/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift b/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift index 78cb6d4e..a506f2fd 100644 --- a/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift +++ b/Projects/Features/Sources/KeymeTests/KeymeTestsView.swift @@ -13,8 +13,6 @@ import ComposableArchitecture import DSKit public struct KeymeTestsView: View { - @State private var showCloseAlert = false - let store: StoreOf public init(store: StoreOf) { @@ -22,11 +20,11 @@ public struct KeymeTestsView: View { } public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(store, observe: { $0 }, send: KeymeTestsFeature.Action.view) { viewStore in ZStack { - KeymeWebView(url: viewStore.url, accessToken: viewStore.authorizationToken ?? "") // TODO: handle it + KeymeWebView(url: viewStore.url, accessToken: viewStore.authorizationToken) .onCloseWebView { - showCloseAlert = true + viewStore.send(.closeButtonTapped) } .onTestSubmitted { testResult in viewStore.send(.showResult(data: testResult)) @@ -34,12 +32,6 @@ public struct KeymeTestsView: View { .toolbar(.hidden, for: .navigationBar) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .alert("", isPresented: $showCloseAlert) { - Button("취소", role: .cancel) { } - Button("종료") { viewStore.send(.close) } - } message: { - Text("테스트를 종료하시겠어요?") - } } } } diff --git a/Projects/Features/Sources/MainPage/MainPageFeature.swift b/Projects/Features/Sources/MainPage/MainPageFeature.swift index 9347c169..68409405 100644 --- a/Projects/Features/Sources/MainPage/MainPageFeature.swift +++ b/Projects/Features/Sources/MainPage/MainPageFeature.swift @@ -35,13 +35,7 @@ public struct MainPageFeature: Reducer { MyPageFeature() } - Reduce { _, action in - switch action { - case .myPage(.setting(.presented(.view(.logout)))): - print("logout from mainpage") - default: - break - } + Reduce { _, _ in return .none } } diff --git a/Projects/Features/Sources/Onboarding/OnboardingFeature.swift b/Projects/Features/Sources/Onboarding/OnboardingFeature.swift index 67c411e4..502bb9ef 100644 --- a/Projects/Features/Sources/Onboarding/OnboardingFeature.swift +++ b/Projects/Features/Sources/Onboarding/OnboardingFeature.swift @@ -63,8 +63,11 @@ public struct OnboardingFeature: Reducer { public var isLoop: Bool = false public var isBlackBackground: Bool = false public var isShared: Bool = false - - public init() { } + + let authorizationToken: String + public init(authorizationToken: String) { + self.authorizationToken = authorizationToken + } } public enum Action: Equatable { @@ -117,12 +120,10 @@ public struct OnboardingFeature: Reducer { return .none case .startButtonDidTap: - // TODO: url 주석단거로 바꾸기 let url = "https://keyme-frontend.vercel.app/test/\(state.testId)" -// let url = "https://keyme-frontend.vercel.app/test/5" - state.keymeTestsState = KeymeTestsFeature.State(url: url) + state.keymeTestsState = KeymeTestsFeature.State(url: url, authorizationToken: state.authorizationToken) // TODO: FIx - case .keymeTests(.presented(.showResult(let data))): + case .keymeTests(.presented(.view(.showResult(let data)))): return .send(.showResult(data: data)) case .showResult(data: let data): diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 89bac0a0..74e76775 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -17,6 +17,12 @@ public struct RootFeature: Reducer { @Dependency(\.notificationManager) private var notificationManager @Dependency(\.keymeAPIManager) private var network + var authorizationToken: String? { + @Dependency(\.keymeAPIManager.authorizationToken) var token + return token + } + + public init() {} public enum State: Equatable { @@ -39,7 +45,7 @@ public struct RootFeature: Reducer { case mainPage(MainPageFeature.Action) case updateState(State) - case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?) + case updateMemberInformation(withMemberData: MemberUpdateDTO.MemberData?, authorizationToken: String) case registerPushNotification } @@ -48,14 +54,16 @@ public struct RootFeature: Reducer { switch action { case .view(.checkUserStatus): let accessToken = userStorage.accessToken - if accessToken == nil { // 로그 아웃 상태 - return .send(.updateState(.needSignIn(SignInFeature.State()))) - } else { // 로그인 상태 + if let accessToken { // 로그인 상태 network.registerAuthorizationToken(accessToken) - return .send(.updateMemberInformation(withMemberData: nil)) + return .send(.updateMemberInformation( + withMemberData: nil, + authorizationToken: accessToken)) + } else { // 로그 아웃 상태 + return .send(.updateState(.needSignIn(SignInFeature.State()))) } - case .updateMemberInformation(let receviedMemberData): + case .updateMemberInformation(let receviedMemberData, let accessToken): return .run(priority: .userInitiated) { send in let memberInformation: MemberUpdateDTO.MemberData if let receviedMemberData { @@ -81,7 +89,9 @@ public struct RootFeature: Reducer { if let userId = memberInformation.id, let nickname = memberInformation.nickname { if memberInformation.isOnboardingClear != true { - await send(.updateState(.needOnboarding(OnboardingFeature.State()))) + await send( + .updateState( + .needOnboarding(OnboardingFeature.State(authorizationToken: accessToken)))) } else { await send(.updateState( .canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) @@ -117,7 +127,7 @@ public struct RootFeature: Reducer { userStorage.accessToken = token network.registerAuthorizationToken(token) - return .send(.updateMemberInformation(withMemberData: nil)) + return .send(.updateMemberInformation(withMemberData: nil, authorizationToken: token)) case .failure: return .none @@ -130,26 +140,38 @@ public struct RootFeature: Reducer { userStorage.accessToken = token network.registerAuthorizationToken(token) - return .send(.updateMemberInformation(withMemberData: nil)) + return .send(.updateMemberInformation(withMemberData: nil, authorizationToken: token)) case .failure: return .none } case .registration(.finishRegisterResponse(let response)): - return .send(.updateMemberInformation(withMemberData: response.data)) + guard let token = authorizationToken else { + // 로그인 재시도 + return gotoSignInState + } + return .send(.updateMemberInformation(withMemberData: response.data, authorizationToken: token)) case .onboarding(.testResult(.closeButtonDidTap)): + guard let token = authorizationToken else { + // 로그인 재시도 + return gotoSignInState + } guard let userId = userStorage.userId, let nickname = userStorage.nickname else { // 멤버 정보 수신 재시도 - return .send(.updateMemberInformation(withMemberData: nil)) + return .send(.updateMemberInformation(withMemberData: nil, authorizationToken: token)) } - return .send(.updateState(.canUseApp(MainPageFeature.State(userId: userId, nickname: nickname)))) case .mainPage(.myPage(.setting(.presented(.view(.logout))))): userStorage.accessToken = nil - return .send(.updateState(.needSignIn(SignInFeature.State()))) + // Logout + return gotoSignInState + + case .mainPage(.home(.requestLogout)): + // Logout + return gotoSignInState default: return .none @@ -168,4 +190,8 @@ public struct RootFeature: Reducer { MainPageFeature() } } + + private var gotoSignInState: Effect { + .send(.updateState(.needSignIn(SignInFeature.State()))) + } } From f750031a2656b43cf44d588d2d4c493e5cb6945c Mon Sep 17 00:00:00 2001 From: Young Bin Lee Date: Wed, 6 Sep 2023 10:42:58 +0900 Subject: [PATCH 30/30] =?UTF-8?q?chore=20#89:=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=B0=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Features/Sources/Onboarding/OnboardingFeature.swift | 2 +- Projects/Features/Sources/Root/RootFeature.swift | 1 - Projects/Features/Sources/ShareSheet/ShareSheetView.swift | 5 ++--- Projects/Network/Sources/DTO/AuthDTO.swift | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Projects/Features/Sources/Onboarding/OnboardingFeature.swift b/Projects/Features/Sources/Onboarding/OnboardingFeature.swift index 502bb9ef..4ff28359 100644 --- a/Projects/Features/Sources/Onboarding/OnboardingFeature.swift +++ b/Projects/Features/Sources/Onboarding/OnboardingFeature.swift @@ -121,7 +121,7 @@ public struct OnboardingFeature: Reducer { case .startButtonDidTap: let url = "https://keyme-frontend.vercel.app/test/\(state.testId)" - state.keymeTestsState = KeymeTestsFeature.State(url: url, authorizationToken: state.authorizationToken) // TODO: FIx + state.keymeTestsState = KeymeTestsFeature.State(url: url, authorizationToken: state.authorizationToken) case .keymeTests(.presented(.view(.showResult(let data)))): return .send(.showResult(data: data)) diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 74e76775..c5787217 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -22,7 +22,6 @@ public struct RootFeature: Reducer { return token } - public init() {} public enum State: Equatable { diff --git a/Projects/Features/Sources/ShareSheet/ShareSheetView.swift b/Projects/Features/Sources/ShareSheet/ShareSheetView.swift index 36de76de..e861b521 100644 --- a/Projects/Features/Sources/ShareSheet/ShareSheetView.swift +++ b/Projects/Features/Sources/ShareSheet/ShareSheetView.swift @@ -15,13 +15,13 @@ struct ActivityViewController: UIViewControllerRepresentable { @Binding var isPresented: Bool var activityItems: [Any] - var applicationActivities: [UIActivity]? = nil + var applicationActivities: [UIActivity]? func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) // Set the completion handler - controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in + controller.completionWithItemsHandler = { (_, _, _, _) in // Dismiss the share sheet when the share action completes (whether successful or not) self.isPresented = false } @@ -34,7 +34,6 @@ struct ActivityViewController: UIViewControllerRepresentable { } } - extension ActivityViewController { struct SharedURL: Identifiable { let id = UUID() diff --git a/Projects/Network/Sources/DTO/AuthDTO.swift b/Projects/Network/Sources/DTO/AuthDTO.swift index d5bea82d..e56dc974 100644 --- a/Projects/Network/Sources/DTO/AuthDTO.swift +++ b/Projects/Network/Sources/DTO/AuthDTO.swift @@ -42,4 +42,3 @@ public struct UserData: Decodable { public struct Token: Decodable { public let accessToken: String } -