diff --git a/MyLuxury/MyLuxury/App/AppComponent.swift b/MyLuxury/MyLuxury/App/AppComponent.swift index 6519d2a..b20324a 100644 --- a/MyLuxury/MyLuxury/App/AppComponent.swift +++ b/MyLuxury/MyLuxury/App/AppComponent.swift @@ -10,28 +10,58 @@ import Data import Domain import Presentation -typealias CoordinatorDependency = AppCoordinatorDependency & LoginCoordinatorDependency & TabBarCoordinatorDependency & HomeCoordinatorDependency & SearchCoordinatorDependency & LibraryCoordinatorDependency +typealias CoordinatorDependency = AppCoordinatorDependency & LoginCoordinatorDependency & TabBarCoordinatorDependency & HomeCoordinatorDependency & SearchCoordinatorDependency & LibraryCoordinatorDependency & PostCoordinatorDependency public class AppComponent: CoordinatorDependency { - var navigationController: UINavigationController + var window: UIWindow public let memberRepository: MemberRepository public let postRepository: PostRepository public let memberUseCase: MemberUseCase public let postUseCase: PostUseCase - public lazy var loginCoordinator: Coordinator = LoginCoordinatorImpl(navigationController: navigationController, dependency: self) - public lazy var tabBarCoordinator: Coordinator = TabBarCoordinatorImpl(navigationController: navigationController, dependency: self) - public lazy var appCoordinator: Coordinator = AppCoordinator(navigationController: self.navigationController, dependency: self) - public lazy var homeCoordinator: Coordinator = HomeCoordinatorImpl(navigationController: self.navigationController, dependency: self) - public lazy var searchCoordinator: Coordinator = SearchCoordinatorImpl(navigationController: self.navigationController, dependency: self) - public lazy var libraryCoordinator: Coordinator = LibraryCoordinatorImpl(navigationController: self.navigationController, dependency: self) + public lazy var appCoordinator: AppCoordinator = AppCoordinatorImpl(dependency: self, window: window) - /// 다른 인스턴스로 실행하고 싶다면 이 생성자에서 해당하는 인스턴스로 바꿔주시면 됩니다. - public init(navigationController: UINavigationController) { + public var loginCoordinator: LoginCoordinator { + get { + return LoginCoordinatorImpl(dependency: self) + } + } + + public var tabBarCoordinator: TabBarCoordinator { + get { + return TabBarCoordinatorImpl(dependency: self) + } + } + + public var homeCoordinator: HomeCoordinator { + get { + return HomeCoordinatorImpl(dependency: self) + } + } + + public var searchCoordinator: SearchCoordinator { + get { + return SearchCoordinatorImpl(dependency: self) + } + } + + public var libraryCoordinator: LibraryCoordinator { + get { + return LibraryCoordinatorImpl(dependency: self) + } + } + + public var postCoordinator: PostCoordinator { + get { + return PostCoordinatorImpl(dependency: self) + } + } + + public init(window: UIWindow) { print("AppComponent init") - self.navigationController = navigationController + self.window = window self.memberRepository = MemberRepositoryImpl() - self.postRepository = PostRepositoryMockImpl() - self.memberUseCase = MemberUseCaseImpl(memberRepository: self.memberRepository) - self.postUseCase = PostUseCaseImpl(postRepository: self.postRepository) + self.postRepository = PostRepositoryImpl() + self.memberUseCase = MemberUseCaseImpl(memberRepository: memberRepository) + self.postUseCase = PostUseCaseImpl(postRepository: postRepository) } } diff --git a/MyLuxury/MyLuxury/App/SceneDelegate.swift b/MyLuxury/MyLuxury/App/SceneDelegate.swift index e52df63..882c5b0 100644 --- a/MyLuxury/MyLuxury/App/SceneDelegate.swift +++ b/MyLuxury/MyLuxury/App/SceneDelegate.swift @@ -19,11 +19,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) let splashView = SplashView(window: window!) { - let navigationController = UINavigationController() - navigationController.navigationBar.isHidden = true - self.window?.rootViewController = navigationController - self.appComponent = AppComponent(navigationController: navigationController) - self.appComponent?.appCoordinator.start() + self.appComponent = AppComponent(window: self.window!) + self.window?.rootViewController = self.appComponent?.appCoordinator.start() } self.window?.addSubview(splashView) self.window?.makeKeyAndVisible() diff --git a/MyLuxury/Presentation/Sources/Presentation/Base/AppCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Base/AppCoordinator.swift new file mode 100644 index 0000000..5d693f7 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Base/AppCoordinator.swift @@ -0,0 +1,73 @@ +// +// AppCoordinator.swift +// MyLuxury +// +// Created by KoSungmin on 10/16/24. +// + +import UIKit +import AuthenticationServices +import FirebaseAuth + +/// 코디네이터는 앱 컴포넌트에서 생성하는 것이기 때문에 +/// 앱의 생명주기 내에서 사라지지 않음. + +@MainActor +public protocol Coordinator: AnyObject { } + +@MainActor +public protocol AppCoordinatorDependency { + var loginCoordinator: LoginCoordinator { get } + var tabBarCoordinator: TabBarCoordinator { get } +} + +public protocol AppCoordinator: Coordinator { + var window: UIWindow { get set } + func start() -> UIViewController +} + +public class AppCoordinatorImpl: AppCoordinator, LoginCoordinatorDelegate, TabBarCoordinatorDelegate { + public var window: UIWindow + public let dependency: AppCoordinatorDependency + public var childCoordinators: [Coordinator] = [] + + public init(dependency: AppCoordinatorDependency, window: UIWindow) { + print("AppCoordinator init") + self.dependency = dependency + self.window = window + } + + public func start() -> UIViewController { + if let _ = Auth.auth().currentUser { + return showMainFlow() + } else { + return showLoginFlow() + } + } + + private func showMainFlow() -> UIViewController { + print("메인 플로우 실행") + let tabBarCoordinator = self.dependency.tabBarCoordinator + tabBarCoordinator.delegate = self + self.childCoordinators.append(tabBarCoordinator) + return tabBarCoordinator.start() + } + + private func showLoginFlow() -> UIViewController { + print("로그인 플로우 실행") + let loginCoordinator = self.dependency.loginCoordinator + loginCoordinator.delegate = self + self.childCoordinators.append(loginCoordinator) + return loginCoordinator.start() + } + + public func didLogin(_ coordinator: LoginCoordinator) { + self.childCoordinators = self.childCoordinators.filter { $0 !== coordinator } + self.window.rootViewController = showMainFlow() + } + + public func didLogout(_ coordinator: TabBarCoordinator) { + self.childCoordinators = self.childCoordinators.filter { $0 !== coordinator } + self.window.rootViewController = showLoginFlow() + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/LoginCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginCoordinator.swift similarity index 56% rename from MyLuxury/Presentation/Sources/Presentation/Coordinator/LoginCoordinator.swift rename to MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginCoordinator.swift index f0a5d68..892dae7 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/LoginCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginCoordinator.swift @@ -9,12 +9,13 @@ import UIKit import Domain @MainActor -protocol LoginCoordinator: Coordinator { +public protocol LoginCoordinator: Coordinator { var delegate: LoginCoordinatorDelegate? { get set } + func start() -> UIViewController } @MainActor -protocol LoginCoordinatorDelegate: AnyObject { +public protocol LoginCoordinatorDelegate: AnyObject { func didLogin(_ coordinator: LoginCoordinator) } @@ -24,26 +25,19 @@ public protocol LoginCoordinatorDependency { } public class LoginCoordinatorImpl: LoginCoordinator, @preconcurrency LoginViewControllerDelegate { - public var navigationController: UINavigationController - public var childCoordinators: [Coordinator] = [] - public let dependency: LoginCoordinatorDependency - weak var delegate: LoginCoordinatorDelegate? + public weak var delegate: LoginCoordinatorDelegate? + private let dependency: LoginCoordinatorDependency - public init(navigationController: UINavigationController, dependency: LoginCoordinatorDependency) { + public init(dependency: LoginCoordinatorDependency) { print("LoginCoordinator init") - self.navigationController = navigationController self.dependency = dependency } - deinit { - print("LoginCoordinator deinit") - } - - public func start() { + public func start() -> UIViewController { let loginVM = LoginViewModel(memberUseCase: self.dependency.memberUseCase) let loginVC = LoginViewController(loginVM: loginVM) loginVC.delegate = self - self.navigationController.viewControllers = [loginVC] + return loginVC } @MainActor diff --git a/MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginViewController.swift index 4715ef4..4e5862b 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Base/Login/LoginViewController.swift @@ -13,7 +13,7 @@ protocol LoginViewControllerDelegate: AnyObject { func login() } -class LoginViewController: UIViewController { +public class LoginViewController: UIViewController { private let rootView: LoginView weak var delegate: LoginViewControllerDelegate? @@ -35,11 +35,11 @@ class LoginViewController: UIViewController { print("LoginViewController deinit") } - override func loadView() { + public override func loadView() { self.view = rootView } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() bindData() } @@ -68,7 +68,7 @@ class LoginViewController: UIViewController { } extension LoginViewController: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return view.window! } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Base/Tab/TabBarCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Base/Tab/TabBarCoordinator.swift new file mode 100644 index 0000000..a0cd5ba --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Base/Tab/TabBarCoordinator.swift @@ -0,0 +1,63 @@ +// +// TabBarCoordinator.swift +// MyLuxury +// +// Created by KoSungmin on 10/16/24. +// + +import UIKit +import Combine + +@MainActor +public protocol TabBarCoordinator: Coordinator { + var delegate: TabBarCoordinatorDelegate? { get set } + func start() -> UIViewController +} + +@MainActor +public protocol TabBarCoordinatorDelegate: AnyObject { + func didLogout(_ coordinator: TabBarCoordinator) +} + +@MainActor +public protocol TabBarCoordinatorDependency { + var homeCoordinator: HomeCoordinator { get } + var searchCoordinator: SearchCoordinator { get } + var libraryCoordinator: LibraryCoordinator { get } +} + +public class TabBarCoordinatorImpl: TabBarCoordinator, @preconcurrency LibraryCoordinatorDelegate { + public weak var delegate: TabBarCoordinatorDelegate? + private var dependency: TabBarCoordinatorDependency + var childCoordinators: [Coordinator] = [] + + public init(dependency: TabBarCoordinatorDependency) { + print("TabBarCoordinatorImpl init") + self.dependency = dependency + } + + public func start() -> UIViewController { + let tabBarController = UITabBarController() + tabBarController.tabBar.tintColor = .white + let homeCoordinator = self.dependency.homeCoordinator + let searchCoordinator = self.dependency.searchCoordinator + let libraryCoordinator = self.dependency.libraryCoordinator + childCoordinators.append(homeCoordinator) + childCoordinators.append(searchCoordinator) + childCoordinators.append(libraryCoordinator) + libraryCoordinator.delegate = self + tabBarController.viewControllers = [ + homeCoordinator.start(), + searchCoordinator.start(), + libraryCoordinator.start() + ] + return tabBarController + } + + @MainActor + public func logout() { + NotificationCenter.default.post(name: .didLogout, object: nil) + self.childCoordinators.removeAll() + self.delegate?.didLogout(self) + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/AppCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Coordinator/AppCoordinator.swift deleted file mode 100644 index 0c78604..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/AppCoordinator.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// AppCoordinator.swift -// MyLuxury -// -// Created by KoSungmin on 10/16/24. -// - -import UIKit -import AuthenticationServices -import FirebaseAuth - -@MainActor -public protocol Coordinator: AnyObject { - var navigationController: UINavigationController { get set } - var childCoordinators: [Coordinator] { get set } - - func start() -} - -@MainActor -public protocol AppCoordinatorDependency { - var loginCoordinator: Coordinator { get } - var tabBarCoordinator: Coordinator { get } -} - -public class AppCoordinator: Coordinator, LoginCoordinatorDelegate, TabBarCoordinatorDelegate { - public var navigationController: UINavigationController - public var childCoordinators: [Coordinator] = [] - public let dependency: AppCoordinatorDependency - - public init(navigationController: UINavigationController, dependency: AppCoordinatorDependency) { - print("AppCoordinator init") - self.navigationController = navigationController - self.dependency = dependency - } - - public func start() { - /// Firebase는 기본적으로 사용자의 인증 상태를 유지 - /// 앱이 새로 설치되었거나 인증 토큰이 만료되면 currentUser가 nil이 되면서 로그인 플로우로 진행 - /// 사용자가 Auth.auth().signOut()을 호출할 경우 로그아웃 상태가 되므로 currentUser가 nil이 됩니다. - /// 새로운 사용자가 로그인하면, currentUser는 새롭게 로그인한 사용자의 정보를 들고 있습니다. - if let user = Auth.auth().currentUser { - /// 사용자가 로그인 상태라면 메인 화면으로 이동 - showMainFlow() - } else { - /// 사용자가 로그아웃 상태라면 - showLoginFlow() - } - } - - private func showMainFlow() { - print("메인 플로우 실행") - guard let tabBarCoordinator = self.dependency.tabBarCoordinator as? TabBarCoordinator else { return } - tabBarCoordinator.delegate = self - tabBarCoordinator.start() - self.childCoordinators.append(tabBarCoordinator) - } - - private func showLoginFlow() { - print("로그인 플로우 실행") - guard let loginCoordinator = self.dependency.loginCoordinator as? LoginCoordinator else { return } - loginCoordinator.delegate = self - loginCoordinator.start() - self.childCoordinators.append(loginCoordinator) - } - - func didLogin(_ coordinator: LoginCoordinator) { - self.childCoordinators = self.childCoordinators.filter { $0 !== coordinator } - self.showMainFlow() - } - - func didLogout(_ coordinator: TabBarCoordinator) { - self.childCoordinators = self.childCoordinators.filter { $0 !== coordinator } - self.showLoginFlow() - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/HomeCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Coordinator/HomeCoordinator.swift deleted file mode 100644 index d9f7f39..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/HomeCoordinator.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// HomeCoordinator.swift -// MyLuxury -// -// Created by KoSungmin on 10/16/24. -// - -import UIKit -import Domain - -public protocol HomeCoordinator: Coordinator { - var navigationController: UINavigationController { get set } -} - -public protocol HomeCoordinatorDependency { - var postUseCase: PostUseCase { get } -} - -public class HomeCoordinatorImpl: HomeCoordinator, @preconcurrency HomeControllerDelegate, @preconcurrency PostViewControllerDelegate { - - public var navigationController: UINavigationController - public var childCoordinators: [Coordinator] = [] - public let dependency: HomeCoordinatorDependency - - public init(navigationController: UINavigationController, dependency: HomeCoordinatorDependency) { - print("HomeCoordinatorImpl init") - self.navigationController = navigationController - self.dependency = dependency - } - - deinit { - print("HomeCoordinatorImpl deinit") - } - - public func start() { - let homeVM = HomeViewModel(postUseCase: self.dependency.postUseCase) - let homeVC = HomeViewController(homeVM: homeVM) - homeVC.delegate = self - self.navigationController = UINavigationController(rootViewController: homeVC) - self.navigationController.isNavigationBarHidden = true - homeVC.tabBarItem = UITabBarItem(title: nil, image: UIImage(systemName: TabBarItem.home.image)?.withTintColor(.gray, renderingMode: .alwaysOriginal), selectedImage: UIImage(systemName: TabBarItem.home.image)?.withTintColor(.white, renderingMode: .alwaysOriginal)) - } - - @MainActor - func goToPost(post: Post) { - let postVM = PostViewModel(post: post, postUseCase: self.dependency.postUseCase) - let postVC = PostViewController(postVM: postVM) - postVC.delegate = self - self.navigationController.pushViewController(postVC, animated: true) - self.navigationController.isNavigationBarHidden = true - } - - @MainActor - func goToBackScreen() { - self.navigationController.popViewController(animated: true) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/TabBarCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Coordinator/TabBarCoordinator.swift deleted file mode 100644 index 91f84cf..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/TabBarCoordinator.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// TabBarCoordinator.swift -// MyLuxury -// -// Created by KoSungmin on 10/16/24. -// - -import UIKit - -@MainActor -protocol TabBarCoordinator: Coordinator { - var delegate: TabBarCoordinatorDelegate? { get set } -} - -@MainActor -protocol TabBarCoordinatorDelegate: AnyObject { - func didLogout(_ coordinator: TabBarCoordinator) -} - -@MainActor -public protocol TabBarCoordinatorDependency { - var homeCoordinator: Coordinator { get } - var searchCoordinator: Coordinator { get } - var libraryCoordinator: Coordinator { get } -} - -/// 메인 플로우의 코디네이터 -public class TabBarCoordinatorImpl: TabBarCoordinator, LibraryCoordinatorDelegate { - public var navigationController: UINavigationController - public var childCoordinators: [Coordinator] = [] - var tabBarController: UITabBarController - weak var delegate: TabBarCoordinatorDelegate? - var dependency: TabBarCoordinatorDependency - - public init(navigationController: UINavigationController, dependency: TabBarCoordinatorDependency) { - self.navigationController = navigationController - self.dependency = dependency - self.tabBarController = UITabBarController() - tabBarController.tabBar.tintColor = .white - } - - deinit { - print("TabBarCoordinatorImpl deinit") - } - - public func start() { - guard let homeCoordinator = dependency.homeCoordinator as? HomeCoordinator else { return } - guard let searchCoordinator = dependency.searchCoordinator as? SearchCoordinator else { return } - guard let libraryCoordinator = dependency.libraryCoordinator as? LibraryCoordinator else { return } - libraryCoordinator.delegate = self - homeCoordinator.start() - searchCoordinator.start() - libraryCoordinator.start() - self.tabBarController.viewControllers = [ - homeCoordinator.navigationController, - searchCoordinator.navigationController, - libraryCoordinator.navigationController - ] - navigationController.viewControllers = [self.tabBarController] - } - - public func logout() { - /// 기존 뷰 계층 초기화 - self.navigationController.viewControllers = [] - /// 기존에 있던 Home, Search, Library 관련 인스턴스에 대한 참조를 끊음. - tabBarController.viewControllers = nil - self.delegate?.didLogout(self) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift new file mode 100644 index 0000000..5de583a --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift @@ -0,0 +1,67 @@ +// +// HomeCoordinator.swift +// MyLuxury +// +// Created by KoSungmin on 10/16/24. +// + +import UIKit +import Domain +import Combine + +public protocol HomeCoordinator: Coordinator { + func start() -> UIViewController +} + +public protocol HomeCoordinatorDependency { + /// 홈 화면 구성에서 홈 게시물 데이터 전체 조회 API가 필요하기 때문에 postUseCase를 알고 있어야 하는 구조입니다. + var postUseCase: PostUseCase { get } + var postCoordinator: PostCoordinator { get } +} + +public class HomeCoordinatorImpl: HomeCoordinator, @preconcurrency HomeViewModelDelegate, @preconcurrency PostCoordinatorDelegate { + private let dependency: HomeCoordinatorDependency + private var navigationController = UINavigationController() + private var cancellables = Set() + var childCoordinators: [Coordinator] = [] + + public init(dependency: HomeCoordinatorDependency) { + print("HomeCoordidnatorImpl init") + self.dependency = dependency + bindNotification() + } + + private func bindNotification() { + NotificationCenter.default.publisher(for: .didLogout) + .sink { [weak self] _ in + guard let self = self else { return } + self.navigationController.viewControllers = [] + } + .store(in: &cancellables) + } + + public func start() -> UIViewController { + let homeVM = HomeViewModel(postUseCase: self.dependency.postUseCase) + let homeVC = HomeViewController(homeVM: homeVM) + homeVM.delegate = self + self.navigationController = UINavigationController(rootViewController: homeVC) + navigationController.navigationBar.isHidden = true + homeVC.tabBarItem = UITabBarItem(title: nil, image: UIImage(systemName: TabBarItem.home.image)?.withTintColor(.gray, renderingMode: .alwaysOriginal), selectedImage: UIImage(systemName: TabBarItem.home.image)?.withTintColor(.white, renderingMode: .alwaysOriginal)) + return navigationController + } + + @MainActor + func goToPost(post: Post) { + let postCoordinator = self.dependency.postCoordinator + childCoordinators.append(postCoordinator) + postCoordinator.delegate = self + let postVC = postCoordinator.start(post: post) + self.navigationController.pushViewController(postVC, animated: true) + } + + @MainActor + public func goToBackScreen() { + self.childCoordinators = self.childCoordinators.filter { !($0 is PostCoordinator) } + self.navigationController.popViewController(animated: true) + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift index 2f06064..051e531 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -9,22 +9,16 @@ import UIKit import Combine import Domain -protocol HomeControllerDelegate: AnyObject { - func goToPost(post: Post) -} - -class HomeViewController: UIViewController { +final class HomeViewController: UIViewController { private let rootView: HomeMainView - weak var delegate: HomeControllerDelegate? private let homeVM: HomeViewModel private var cancellabes = Set() init(homeVM: HomeViewModel) { - + print("HomeViewController init") self.homeVM = homeVM self.rootView = HomeMainView(homeVM: homeVM) super.init(nibName: nil, bundle: nil) - print("HomeViewController init, 메모리 주소: \(Unmanaged.passUnretained(self).toOpaque())") } required init?(coder: NSCoder) { @@ -32,8 +26,7 @@ class HomeViewController: UIViewController { } deinit { -// print("HomeViewController deinit") - print("HomeViewController deinit, 메모리 주소: \(Unmanaged.passUnretained(self).toOpaque())") + print("HomeViewController deinit") } /// 첫 번째로 호출 @@ -67,7 +60,7 @@ class HomeViewController: UIViewController { case .getHomePostData: self.rootView.contentView.homePostData = self.homeVM.homePostData case .goToPost(let post): - self.delegate?.goToPost(post: post) + self.homeVM.delegate?.goToPost(post: post) } }.store(in: &cancellabes) } diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift index 586ecda..71eab26 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift @@ -9,12 +9,17 @@ import Foundation import Combine import Domain -public class HomeViewModel { +protocol HomeViewModelDelegate: AnyObject { + func goToPost(post: Post) +} + +class HomeViewModel { private let postUseCase: PostUseCase private let output: PassthroughSubject = .init() private let input: PassthroughSubject = .init() private var cancellables = Set() var homePostData: HomePostData? = nil + weak var delegate: HomeViewModelDelegate? init(postUseCase: PostUseCase) { print("HomeViewModel init") diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/LibraryCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryCoordinator.swift similarity index 60% rename from MyLuxury/Presentation/Sources/Presentation/Coordinator/LibraryCoordinator.swift rename to MyLuxury/Presentation/Sources/Presentation/Library/LibraryCoordinator.swift index 0878535..579e9e4 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/LibraryCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryCoordinator.swift @@ -7,44 +7,50 @@ import UIKit import Domain +import Combine public protocol LibraryCoordinator: Coordinator { - var navigationController: UINavigationController { get set } var delegate: LibraryCoordinatorDelegate? { get set } + func start() -> UIViewController } -@MainActor public protocol LibraryCoordinatorDelegate: AnyObject { func logout() } +@MainActor public protocol LibraryCoordinatorDependency { var memberUseCase: MemberUseCase { get } } -public class LibraryCoordinatorImpl: LibraryCoordinator, @preconcurrency LibraryControllerDelegate { - public var navigationController: UINavigationController - public var childCoordinators: [Coordinator] = [] - public let dependency: LibraryCoordinatorDependency +public class LibraryCoordinatorImpl: LibraryCoordinator, @preconcurrency LibraryViewModelDelegate { public weak var delegate: LibraryCoordinatorDelegate? + private let dependency: LibraryCoordinatorDependency + private var navigationController = UINavigationController() + private var cancellables = Set() - public init(navigationController: UINavigationController, dependency: LibraryCoordinatorDependency) { + public init(dependency: LibraryCoordinatorDependency) { print("LibraryCoordinatorImpl init") - self.navigationController = navigationController self.dependency = dependency + bindNotification() } - deinit { - print("LibraryCoordinatorImpl deinit") + private func bindNotification() { + NotificationCenter.default.publisher(for: .didLogout) + .sink { [weak self] _ in + guard let self = self else { return } + self.navigationController.viewControllers = [] + }.store(in: &cancellables) } - public func start() { + public func start() -> UIViewController { let libraryVM = LibraryViewModel(memberUseCase: self.dependency.memberUseCase) let libraryVC = LibraryViewController(libraryVM: libraryVM) - libraryVC.delegate = self + libraryVM.delegate = self self.navigationController = UINavigationController(rootViewController: libraryVC) - self.navigationController.isNavigationBarHidden = true + self.navigationController.navigationBar.isHidden = true libraryVC.tabBarItem = UITabBarItem(title: nil, image: UIImage(systemName: TabBarItem.library.image)?.withTintColor(.gray, renderingMode: .alwaysOriginal), selectedImage: UIImage(systemName: TabBarItem.library.image)?.withTintColor(.white, renderingMode: .alwaysOriginal)) + return navigationController } @MainActor diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewController.swift index 9a9b227..eb279fa 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewController.swift @@ -8,13 +8,8 @@ import UIKit import Combine -protocol LibraryControllerDelegate: AnyObject { - func goToLoginPage() -} - -class LibraryViewController: UIViewController { - let libraryVM: LibraryViewModel - weak var delegate: LibraryControllerDelegate? +final class LibraryViewController: UIViewController { + private let libraryVM: LibraryViewModel private let rootView: LibraryView private var cancellables = Set() @@ -50,7 +45,7 @@ class LibraryViewController: UIViewController { guard let self = self else { return } switch event { case .logoutSuccess: - self.delegate?.goToLoginPage() + self.libraryVM.delegate?.goToLoginPage() case .logoutFailed(let message): self.showLogoutFailedAlert(message: message) } diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewModel.swift index 9328d2b..9961199 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewModel.swift @@ -9,11 +9,16 @@ import UIKit import Combine import Domain -public class LibraryViewModel { +protocol LibraryViewModelDelegate: AnyObject { + func goToLoginPage() +} + +class LibraryViewModel { private let memberUseCase: MemberUseCase private let output: PassthroughSubject = .init() private let input: PassthroughSubject = .init() private var cancellables = Set() + weak var delegate: LibraryViewModelDelegate? init(memberUseCase: MemberUseCase) { print("LibraryViewModel init") diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift new file mode 100644 index 0000000..a40a6e5 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift @@ -0,0 +1,43 @@ +// +// PostCoordinator.swift +// Presentation +// +// Created by KoSungmin on 11/23/24. +// + +import UIKit +import Domain + +public protocol PostCoordinatorDelegate: AnyObject { + func goToBackScreen() +} + +public protocol PostCoordinator: Coordinator { + func start(post: Post) -> UIViewController + var delegate: PostCoordinatorDelegate? { get set } +} + +public protocol PostCoordinatorDependency { + var postUseCase: PostUseCase { get } +} + +public class PostCoordinatorImpl: PostCoordinator, @preconcurrency PostViewModelDelegate { + private var dependency: PostCoordinatorDependency + public weak var delegate: PostCoordinatorDelegate? + + public init(dependency: PostCoordinatorDependency) { + print("PostCoordinatorImpl init") + self.dependency = dependency + } + + public func start(post: Post) -> UIViewController { + let postVM = PostViewModel(post: post, postUseCase: self.dependency.postUseCase) + postVM.delegate = self + let postVC = PostViewController(postVM: postVM) + return postVC + } + + func goToBackScreen() { + self.delegate?.goToBackScreen() + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift index 6c855b8..041ef43 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift @@ -9,13 +9,8 @@ import UIKit import Combine import Domain -protocol PostViewControllerDelegate: AnyObject { - func goToBackScreen() -} - final class PostViewController: UIViewController { private let rootView: PostView - weak var delegate: PostViewControllerDelegate? private let postVM: PostViewModel private var cancellable = Set() @@ -35,28 +30,27 @@ final class PostViewController: UIViewController { print("PostViewController deinit") } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true } - override func loadView() { + public override func loadView() { self.view = rootView } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() bindData() } - override func viewWillDisappear(_ animated: Bool) { + public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) tabBarController?.tabBar.isHidden = false } - override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) -// input.send(.viewLoaded) postVM.sendInputEvent(input: .viewLoaded) } @@ -68,7 +62,7 @@ final class PostViewController: UIViewController { guard let self = self else { return } switch event { case .goToBackScreen: - self.delegate?.goToBackScreen() + self.postVM.delegate?.goToBackScreen() case .getPostOneData: self.rootView.post = self.postVM.post } diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift index 080cce1..fdaf8fe 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift @@ -9,11 +9,16 @@ import UIKit import Domain import Combine +protocol PostViewModelDelegate: AnyObject { + func goToBackScreen() +} + class PostViewModel { let postUseCase: PostUseCase private let output: PassthroughSubject = .init() private let input: PassthroughSubject = .init() var cancellables = Set() + weak var delegate: PostViewModelDelegate? let postId: String var post: Post? = nil diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift similarity index 55% rename from MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift rename to MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift index 59c37bc..0c60ea1 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift @@ -7,47 +7,52 @@ import UIKit import Domain +import Combine public protocol SearchCoordinator: Coordinator { - var navigationController: UINavigationController { get set } + func start() -> UIViewController } public protocol SearchCoordinatorDependency { var postUseCase: PostUseCase { get } + var postCoordinator: PostCoordinator { get } } -public class SearchCoordinatorImpl: SearchCoordinator, @preconcurrency SearchGridViewControllerDelegate, @preconcurrency SearchResultViewControllerDelegate, @preconcurrency PostViewControllerDelegate { +public class SearchCoordinatorImpl: SearchCoordinator, @preconcurrency SearchViewModelDelegate, @preconcurrency PostCoordinatorDelegate { + private let dependency: SearchCoordinatorDependency + private var navigationController = UINavigationController() + private var cancellables = Set() + var childCoordinators: [Coordinator] = [] - public var navigationController: UINavigationController - public var childCoordinators: [Coordinator] = [] - public let dependency: SearchCoordinatorDependency - - public init(navigationController: UINavigationController, dependency: SearchCoordinatorDependency) { + public init(dependency: SearchCoordinatorDependency) { print("SearchCoordinatorImpl init") - self.navigationController = navigationController self.dependency = dependency + bindNotification() } - deinit { - print("SearchCoordinatorImpl deinit") + private func bindNotification() { + NotificationCenter.default.publisher(for: .didLogout) + .sink { [weak self] _ in + guard let self = self else { return } + self.navigationController.viewControllers = [] + }.store(in: &cancellables) } - public func start() { + public func start() -> UIViewController { let searchVM = SearchViewModel(postUseCase: self.dependency.postUseCase) + searchVM.delegate = self let searchGridVC = SearchGridViewController(searchVM: searchVM) - searchGridVC.delegate = self self.navigationController = UINavigationController(rootViewController: searchGridVC) - self.navigationController.isNavigationBarHidden = true + self.navigationController.navigationBar.isHidden = true searchGridVC.tabBarItem = UITabBarItem(title: nil, image: UIImage(systemName: TabBarItem.search.image)?.withTintColor(.gray, renderingMode: .alwaysOriginal), selectedImage: UIImage(systemName: TabBarItem.search.image)?.withTintColor(.white, renderingMode: .alwaysOriginal)) + return navigationController } @MainActor func goToSearchResultView(searchVM: SearchViewModel) { + /// searchGridVC와 sesarchResultVC는 같은 뷰모델을 공유 let searchResultVC = SearchResultViewController(searchVM: searchVM) - searchResultVC.delegate = self - /// 자연스럽게 이동하도록 설정 searchResultVC.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - /// 탭바를 유지하기 위해서 .overCurrentContext로 설정 searchResultVC.modalPresentationStyle = .overCurrentContext self.navigationController.present(searchResultVC, animated: true) } @@ -59,15 +64,16 @@ public class SearchCoordinatorImpl: SearchCoordinator, @preconcurrency SearchGri @MainActor func goToPostView(post: Post) { - let postVM = PostViewModel(post: post, postUseCase: self.dependency.postUseCase) - let postVC = PostViewController(postVM: postVM) - postVC.delegate = self + let postCoordinator = self.dependency.postCoordinator + postCoordinator.delegate = self + childCoordinators.append(postCoordinator) + let postVC = postCoordinator.start(post: post) self.navigationController.pushViewController(postVC, animated: true) - self.navigationController.isNavigationBarHidden = true } @MainActor - func goToBackScreen() { + public func goToBackScreen() { + self.childCoordinators = self.childCoordinators.filter { !($0 is PostCoordinator) } self.navigationController.popViewController(animated: true) } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift index 94557c8..90dc300 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift @@ -9,15 +9,9 @@ import UIKit import Combine import Domain -protocol SearchGridViewControllerDelegate: AnyObject { - func goToSearchResultView(searchVM: SearchViewModel) - func goToPostView(post: Post) -} - -class SearchGridViewController: UIViewController { +final class SearchGridViewController: UIViewController { private let rootView: SearchGridView private let searchVM: SearchViewModel - weak var delegate: SearchGridViewControllerDelegate? private var cancellables = Set() init(searchVM: SearchViewModel) { @@ -57,11 +51,11 @@ class SearchGridViewController: UIViewController { guard let self = self else { return } switch event { case .goToSearchResultView: - self.delegate?.goToSearchResultView(searchVM: self.searchVM) + self.searchVM.delegate?.goToSearchResultView(searchVM: searchVM) case .getSearchGridPosts: self.rootView.posts = self.searchVM.searchGridPosts case .goToPostView(let post): - self.delegate?.goToPostView(post: post) + self.searchVM.delegate?.goToPostView(post: post) default: break } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift index 2f63176..c4ac10d 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift @@ -9,14 +9,9 @@ import UIKit import Domain import Combine -protocol SearchResultViewControllerDelegate: AnyObject { - func goBackToResultGridView() -} - -class SearchResultViewController: UIViewController { +final class SearchResultViewController: UIViewController { private let rootView: SearchResultView private let searchVM: SearchViewModel - weak var delegate: SearchResultViewControllerDelegate? private var cancellables = Set() init(searchVM: SearchViewModel) { @@ -57,7 +52,7 @@ class SearchResultViewController: UIViewController { guard let self = self else { return } switch event { case .goBackToSearchResultView: - self.delegate?.goBackToResultGridView() + self.searchVM.delegate?.goBackToResultGridView() default: break } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift similarity index 91% rename from MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift rename to MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift index eddd986..23465ac 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift @@ -9,11 +9,18 @@ import UIKit import Combine import Domain -public class SearchViewModel { +protocol SearchViewModelDelegate: AnyObject { + func goToSearchResultView(searchVM: SearchViewModel) + func goToPostView(post: Post) + func goBackToResultGridView() +} + +class SearchViewModel { let postUseCase: PostUseCase private let output: PassthroughSubject = .init() private let input: PassthroughSubject = .init() var cancellables = Set() + weak var delegate: SearchViewModelDelegate? var searchGridPosts: [Post] = [] diff --git a/MyLuxury/Presentation/Sources/Presentation/Utils/Extensions/Notification.swift b/MyLuxury/Presentation/Sources/Presentation/Utils/Extensions/Notification.swift new file mode 100644 index 0000000..4332042 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Utils/Extensions/Notification.swift @@ -0,0 +1,12 @@ +// +// Notification.swift +// Presentation +// +// Created by KoSungmin on 11/25/24. +// + +import UIKit + +extension Notification.Name { + static let didLogout = Notification.Name("didLogout") +}