diff --git a/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate b/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate index bb10369..8165c44 100644 Binary files a/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate and b/AcaMate.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/AcaMate/0. Setup/AppDelegate.swift b/AcaMate/0. Setup/AppDelegate.swift index 91b7438..961b9d4 100644 --- a/AcaMate/0. Setup/AppDelegate.swift +++ b/AcaMate/0. Setup/AppDelegate.swift @@ -44,8 +44,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele } center.delegate = self -//MARK: - 네트워크 모니터 초기화 -// _ = NetworkMonitor.shared + //MARK: - 네트워크 모니터 초기화 + _ = NetworkMonitor.shared printLog("End Set AppDelegate") return true diff --git a/AcaMate/1. View/0. Common/NavigationView.swift b/AcaMate/1. View/0. Common/NavigationView.swift new file mode 100644 index 0000000..9024bc6 --- /dev/null +++ b/AcaMate/1. View/0. Common/NavigationView.swift @@ -0,0 +1,87 @@ +// +// NavigationView.swift +// AcaMate +// +// Created by Sean Kim on 12/12/24. +// + +import SwiftUI +import Combine + + + +struct NavigationView: View { + @State private var naviState : NaviState = .init(act: .NONE, path: .Intro) + @State private var history: [PathName] = [.Intro] + + + var body: some View { + ZStack { + switch naviState.path { + case .NONE: + EmptyView() + case .Intro: + IntroView(naviState: $naviState) + case .Login : + LoginView(naviState: $naviState) + case .Main: + EmptyView() + } + } + .onChange(of: naviState) { old, new in + switch new.act { + case .NONE: + break + case .ADD: + addHistory(path: new.path) + case .POP: + popHistory() + case .RESET: + resetHistory(path: new.path) + case .MOVE: + moveHistory(path: new.path) + } + + // LOG + printLog("\(old.path) => \(new.path)") + showHistory() + } + .fullView(.Normal.normal) + .setAlert() + .setNetwork() + } + + private func addHistory(path: PathName) { + history.append(path) + } + + private func popHistory() { + history.removeLast() + naviState.set(act: .NONE, path: history.last ?? .NONE) + } + + private func resetHistory(path: PathName) { + history.removeAll() + addHistory(path: path) + } + + private func moveHistory(path: PathName) { + if path == .NONE { + naviState.set(act: .RESET, path: history.first ?? .Main) + } + if history.contains(path) { + let remove = history.count - history.firstIndex(of: path)! - 1 + history.removeLast(remove) + if remove > 0 { + naviState.set(act: .NONE, path: path) + return + } + } + naviState.set(act: .RESET, path: path) + } + + private func showHistory() { + printLog(history) + } + +} diff --git a/AcaMate/1. View/1. Intro & Login/IntroView.swift b/AcaMate/1. View/1. Intro & Login/IntroView.swift index 47ad830..4d21e43 100644 --- a/AcaMate/1. View/1. Intro & Login/IntroView.swift +++ b/AcaMate/1. View/1. Intro & Login/IntroView.swift @@ -9,67 +9,80 @@ import SwiftUI import Combine struct IntroView: View { + @EnvironmentObject var alertController: AlertController @State var cancellables: Set = [] + @Binding var naviState : NaviState var body: some View { - NavigationStack { - VStack(spacing: 0) { - Spacer() - .frame(height: 100) + VStack(spacing: 0) { + Spacer() + .frame(height: 100) + Image("Team_Icon") + .resizable() + .frame(width: 200, height: 200) + .background(.white) + .border(.black) + .padding() + Spacer() + HStack(spacing: 4) { Image("Team_Icon") .resizable() - .frame(width: 200, height: 200) - .background(.white) - .border(.black) - .padding() - Spacer() - HStack(spacing: 4) { - Image("Team_Icon") - .resizable() - .frame(width: 24, height: 24) - Text("STEIN") - .font(.nps(font: .bold, size: 16)) - .foregroundStyle(Color(.Text.detail)) - } - .padding(.bottom,12) - Text("Copyright © Team.Stein") - .font(.nps(font: .regular, size: 14)) + .frame(width: 24, height: 24) + Text("STEIN") + .font(.nps(font: .bold, size: 16)) .foregroundStyle(Color(.Text.detail)) - .padding(.bottom,50) - } - .fullView(.Normal.normal) - .onAppear { - printLog("IntroView_onAppear") - loadVersion() - .sink { completion in - switch completion { - case .failure(let error): - printLog(error) - case .finished: break - } - } receiveValue: { version in - switch compareVersion(version.force_ver, currentVersion()){ - case .bigger: - printLog("강제 업데이트") - default: - switch compareVersion(version.final_ver, currentVersion()) { - case .bigger: - if version.choice_update_yn { - printLog("선택 업데이트") - } - else { - printLog("정상 동작") - } - default: - printLog("선택 업데이트 넘어감") - } - } - } - .store(in: &cancellables) - - } + .padding(.bottom,12) + Text("Copyright © Team.Stein") + .font(.nps(font: .regular, size: 14)) + .foregroundStyle(Color(.Text.detail)) + .padding(.bottom,50) } + + .onAppear { + printLog("IntroView_onAppear") + subscribeAlertAction() + loadVersion() + .sink { completion in + switch completion { + case .failure(let error): + printLog(error) + case .finished: break + } + } receiveValue: { version in + let compareForce = compareVersion(version.force_ver, currentVersion()) + let compareChoice = compareVersion(version.final_ver, currentVersion()) + if compareForce == .bigger { + alertController.alertData = SetAlertData().setForceUpdate( + action: alertController.alertAction + ) + alertController.showAlert.toggle() + } else if compareChoice == .bigger && version.choice_update_yn { + alertController.alertData = SetAlertData().setSelectUpdate( + action: alertController.alertAction + ) + alertController.showAlert.toggle() + } else { + naviState.set(act: .RESET, path: .Login) + } + } + .store(in: &cancellables) + + + } + } + + private func subscribeAlertAction() { + alertController.alertAction + .compactMap { $0 } + .sink { action in + if action == "updateNow" { + exit(1) + //MARK: - TODO (앱스토어 이동 로직 넣을 것) + } else { + naviState.set(act: .RESET, path: .Login) + } + }.store(in: &cancellables) } @@ -101,15 +114,15 @@ struct IntroView: View { return currentVer } - + private func versionChange(ver: String) -> [Int] { return ver.components(separatedBy: ["."]).map {Int($0) ?? 0} } - - - } -#Preview { - IntroView() -} + + + +//#Preview { +// IntroView(path: $NavigationPath()) +//} diff --git a/AcaMate/1. View/1. Intro & Login/LoginView.swift b/AcaMate/1. View/1. Intro & Login/LoginView.swift index a3a119e..96535fa 100644 --- a/AcaMate/1. View/1. Intro & Login/LoginView.swift +++ b/AcaMate/1. View/1. Intro & Login/LoginView.swift @@ -8,6 +8,9 @@ import SwiftUI struct LoginView: View { + @EnvironmentObject var alertController: AlertController + @Binding var naviState : NaviState + var body: some View { VStack(spacing: 0) { Image("Team_Icon") @@ -34,12 +37,21 @@ struct LoginView: View { } } - + Button { + alertController.alertData = SetAlertData().setTest() + alertController.showAlert.toggle() +// naviState.set(act: .MOVE,path: .Intro) + + } label: { + Text("111111111") + } + .padding() } .fullView(.Normal.normal) + } } -#Preview { - LoginView() -} +//#Preview { +// LoginView() +//} diff --git a/AcaMate/3. Controller/AlertController.swift b/AcaMate/3. Controller/AlertController.swift new file mode 100644 index 0000000..03b2d8f --- /dev/null +++ b/AcaMate/3. Controller/AlertController.swift @@ -0,0 +1,19 @@ +// +// AlertController.swift +// AcaMate +// +// Created by Sean Kim on 12/13/24. +// + +import SwiftUI +import Combine + + +class AlertController: ObservableObject { + @Published var showAlert: Bool = false + var alertData: AlertData = .init(body: "") + + let alertAction = CurrentValueSubject(nil) + + +} diff --git a/AcaMate/4. Model/Alert.swift b/AcaMate/4. Model/Alert.swift index 6c69cc5..6fcba27 100644 --- a/AcaMate/4. Model/Alert.swift +++ b/AcaMate/4. Model/Alert.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Combine struct AlertData { var title: String @@ -24,3 +25,62 @@ struct ButtonType { var role: ButtonRole? var function: (()->())? } + + + +struct SetAlertData { + func setTest() -> AlertData { + return AlertData(title: "TEST", body: "TEST용 알럿 입니다.", + button: [ + ButtonType(name: "테스트", role: .cancel, + function: { + printLog("테스트 중입니다.") + }) + ]) + } + + + /// 강제 업데이트 안내 + func setForceUpdate(action: CurrentValueSubject) -> AlertData { + return AlertData(title: "업데이트 안내", + body: """ + 앱 최신 버전이 나왔습니다. + 앱을 종료 후 업데이트를 해주세요. + """, + button: [ + ButtonType(name: "업데이트 하기", role: .none, + function: { + action.send("updateNow") + }) + ]) + } + + /// 선택 업데이트 안내 + func setSelectUpdate (action: CurrentValueSubject) -> AlertData { + return AlertData(title: "업데이트 안내", + body: """ + 앱 최신 버전이 새로 나왔습니다. + 지금 업데이트 하시겠어요? + """, + button: [ + ButtonType(name: "지금 업데이트", role: .cancel, + function: { + action.send("updateNow") + }), + ButtonType(name: "나중에", role: .none, + function: { + action.send("updateLater") + }), + ]) + } + + /// 네트워크 문제 발생 시 + func setErrorNetwork() -> AlertData { + return AlertData(title: "네트워크 오류", body: "네트워크가 불안정합니다.\n확인 후 다시 시도해주세요.", + button: [ + ButtonType(name: "확인", role: .cancel, function: nil) + ]) + } + +} + diff --git a/AcaMate/4. Model/Navigation.swift b/AcaMate/4. Model/Navigation.swift new file mode 100644 index 0000000..3b974a9 --- /dev/null +++ b/AcaMate/4. Model/Navigation.swift @@ -0,0 +1,47 @@ +// +// Navigation.swift +// AcaMate +// +// Created by Sean Kim on 12/13/24. +// + +import Foundation + + + +struct NaviState: Equatable { + var act: NaviAction + var path: PathName + + static func == (lhs: NaviState, rhs: NaviState) -> Bool { + return lhs.act == rhs.act && lhs.path == rhs.path + } + + mutating func set(act: NaviAction = .ADD, path: PathName = .NONE) { + self.act = act + self.path = path + } +} + +enum NaviAction: Hashable { + /// 한 계층 추가 + case ADD + /// 전체 삭제 후 추가 + case RESET + /// 한 계층 빼기 + case POP + /// 위치로 이동 (path == NONE인 경우 최상단) + case MOVE + /// 아무 동작 안함 + case NONE + + /// FIRST = 가장 메인, MOVE = 그곳으로 +} + +enum PathName: Hashable { + case Intro + case Login + case Main + + case NONE +} diff --git a/AcaMate/5. Modifier/Network.swift b/AcaMate/5. Modifier/Network.swift new file mode 100644 index 0000000..98df925 --- /dev/null +++ b/AcaMate/5. Modifier/Network.swift @@ -0,0 +1,27 @@ +// +// Network.swift +// AcaMate +// +// Created by Sean Kim on 12/10/24. +// + +import Network +import Combine + +class NetworkMonitor: ObservableObject { + static let shared = NetworkMonitor() + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue.global(qos: .background) + + @Published var isConnected: Bool = true + + private init() { + monitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + self?.isConnected = (path.status == .satisfied) + } + } + monitor.start(queue: queue) + } +} diff --git a/AcaMate/5. Modifier/View.swift b/AcaMate/5. Modifier/View.swift index 3656f7b..89b8997 100644 --- a/AcaMate/5. Modifier/View.swift +++ b/AcaMate/5. Modifier/View.swift @@ -6,15 +6,66 @@ // import SwiftUI +import Combine + +struct NetworkModifier: ViewModifier { + @ObservedObject private var networkMonitor = NetworkMonitor.shared + @EnvironmentObject var alertController: AlertController + + func body(content: Content) -> some View { + content + .onChange(of: networkMonitor.isConnected) { _ , new in + if !new { + alertController.alertData = SetAlertData().setErrorNetwork() + alertController.showAlert.toggle() + } + } + } +} + +struct AlertModifier: ViewModifier { + @EnvironmentObject var controller: AlertController + func body(content: Content) -> some View { + content + .alert(controller.alertData.title, + isPresented: $controller.showAlert, + presenting: $controller.alertData) { data in + let btnCount = data.button.count + ForEach(0 ..< btnCount, id: \.self) { index in + let btn = data.wrappedValue.button[index] + Button(role: btn.role) { + if let function = btn.function { function() } + } label: { + Text("\(btn.name)") + } + } + } message: { data in + Text("\(data.body.wrappedValue)") + } + } +} extension View { - func fullView(_ backColor: Color) -> some View{ + func fullView(_ backColor: Color) -> some View { return self .frame(maxWidth: .infinity, maxHeight: .infinity) .background(backColor) } + func setAlert() -> some View { + self.modifier(AlertModifier()) + } + func setNetwork() -> some View { + self.modifier(NetworkModifier()) + } + func endTextEditing() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } + + func setNavigaion() -> some View { + self + .navigationBarBackButtonHidden(true) + .toolbar(.hidden, for: .navigationBar) + } } diff --git a/AcaMate/AcaMateApp.swift b/AcaMate/AcaMateApp.swift index 5454b18..f7adeeb 100644 --- a/AcaMate/AcaMateApp.swift +++ b/AcaMate/AcaMateApp.swift @@ -15,6 +15,7 @@ import KakaoSDKAuth struct AcaMateApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var cancellables: Set = [] + var alertController = AlertController() init() { printLog("APP INIT") @@ -22,11 +23,13 @@ struct AcaMateApp: App { var body: some Scene { WindowGroup { - IntroView().onOpenURL { url in - if (AuthApi.isKakaoTalkLoginUrl(url)) { - _ = AuthController.handleOpenUrl(url: url) + NavigationView() + .onOpenURL { url in + if (AuthApi.isKakaoTalkLoginUrl(url)) { + _ = AuthController.handleOpenUrl(url: url) + } } - } + .environmentObject(self.alertController) } } }