diff --git a/BeMyPlan/.swiftlint.yml b/BeMyPlan/.swiftlint.yml index 32d40280..941f3582 100644 --- a/BeMyPlan/.swiftlint.yml +++ b/BeMyPlan/.swiftlint.yml @@ -48,6 +48,7 @@ disabled_rules: # Default Rules에서 비활성화할 규칙 - control_statement - force_cast - comment_spacing + - for_where ### 배열을 사용해 warning과 error의 수준을 모두 설정할 수 있습니다. type_body_length: diff --git a/BeMyPlan/BeMyPlan.xcodeproj/project.pbxproj b/BeMyPlan/BeMyPlan.xcodeproj/project.pbxproj index bde2e263..36851b47 100644 --- a/BeMyPlan/BeMyPlan.xcodeproj/project.pbxproj +++ b/BeMyPlan/BeMyPlan.xcodeproj/project.pbxproj @@ -196,6 +196,13 @@ FDCE38282799BACB0042DC54 /* Indicator.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDCE38272799BACB0042DC54 /* Indicator.storyboard */; }; FDCE382A2799BAD80042DC54 /* IndicatorVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCE38292799BAD80042DC54 /* IndicatorVC.swift */; }; FDCE382C2799BBC90042DC54 /* showIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCE382B2799BBC90042DC54 /* showIndicator.swift */; }; + FDF7FEC327A7E14600E3AB62 /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FEC227A7E14600E3AB62 /* Presentable.swift */; }; + FDF7FEC527A7E14F00E3AB62 /* CoordinatorFinishOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FEC427A7E14F00E3AB62 /* CoordinatorFinishOutput.swift */; }; + FDF7FEC727A7E15B00E3AB62 /* BaseControllable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FEC627A7E15B00E3AB62 /* BaseControllable.swift */; }; + FDF7FEC927A7E16100E3AB62 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FEC827A7E16100E3AB62 /* Coordinator.swift */; }; + FDF7FECC27A7E1DD00E3AB62 /* LaunchInstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FECB27A7E1DD00E3AB62 /* LaunchInstructor.swift */; }; + FDF7FECF27A7E21600E3AB62 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FECE27A7E21600E3AB62 /* Router.swift */; }; + FDF7FED227A7E2B900E3AB62 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7FED127A7E2B900E3AB62 /* BaseCoordinator.swift */; }; FDF980BC2786CE660009A14E /* paybooc_OTF_Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = FDF980BA2786CE660009A14E /* paybooc_OTF_Bold.otf */; }; FDF980BD2786CE660009A14E /* Spoqa_Han_Sans_Neo_Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = FDF980BB2786CE660009A14E /* Spoqa_Han_Sans_Neo_Bold.otf */; }; FDF980BF2786CE700009A14E /* Spoqa_Han_Sans_Neo_Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = FDF980BE2786CE700009A14E /* Spoqa_Han_Sans_Neo_Regular.otf */; }; @@ -445,6 +452,13 @@ FDCE38272799BACB0042DC54 /* Indicator.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Indicator.storyboard; sourceTree = ""; }; FDCE38292799BAD80042DC54 /* IndicatorVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorVC.swift; sourceTree = ""; }; FDCE382B2799BBC90042DC54 /* showIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = showIndicator.swift; sourceTree = ""; }; + FDF7FEC227A7E14600E3AB62 /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; + FDF7FEC427A7E14F00E3AB62 /* CoordinatorFinishOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorFinishOutput.swift; sourceTree = ""; }; + FDF7FEC627A7E15B00E3AB62 /* BaseControllable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseControllable.swift; sourceTree = ""; }; + FDF7FEC827A7E16100E3AB62 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + FDF7FECB27A7E1DD00E3AB62 /* LaunchInstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchInstructor.swift; sourceTree = ""; }; + FDF7FECE27A7E21600E3AB62 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + FDF7FED127A7E2B900E3AB62 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; FDF980BA2786CE660009A14E /* paybooc_OTF_Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = paybooc_OTF_Bold.otf; sourceTree = ""; }; FDF980BB2786CE660009A14E /* Spoqa_Han_Sans_Neo_Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Spoqa_Han_Sans_Neo_Bold.otf; sourceTree = ""; }; FDF980BE2786CE700009A14E /* Spoqa_Han_Sans_Neo_Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Spoqa_Han_Sans_Neo_Regular.otf; sourceTree = ""; }; @@ -1196,6 +1210,10 @@ FD47D76627A50C1300272352 /* Coordinator */ = { isa = PBXGroup; children = ( + FDF7FED027A7E2A500E3AB62 /* Coordinators */, + FDF7FEC127A7E13900E3AB62 /* Protocols */, + FDF7FECA27A7E1D100E3AB62 /* Enums */, + FDF7FECD27A7E1E700E3AB62 /* Router */, FDB34C3B27A6D4AD0074B6CE /* Factory */, ); path = Coordinator; @@ -1253,6 +1271,41 @@ path = Indicator; sourceTree = ""; }; + FDF7FEC127A7E13900E3AB62 /* Protocols */ = { + isa = PBXGroup; + children = ( + FDF7FEC227A7E14600E3AB62 /* Presentable.swift */, + FDF7FEC427A7E14F00E3AB62 /* CoordinatorFinishOutput.swift */, + FDF7FEC627A7E15B00E3AB62 /* BaseControllable.swift */, + FDF7FEC827A7E16100E3AB62 /* Coordinator.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + FDF7FECA27A7E1D100E3AB62 /* Enums */ = { + isa = PBXGroup; + children = ( + FDF7FECB27A7E1DD00E3AB62 /* LaunchInstructor.swift */, + ); + path = Enums; + sourceTree = ""; + }; + FDF7FECD27A7E1E700E3AB62 /* Router */ = { + isa = PBXGroup; + children = ( + FDF7FECE27A7E21600E3AB62 /* Router.swift */, + ); + path = Router; + sourceTree = ""; + }; + FDF7FED027A7E2A500E3AB62 /* Coordinators */ = { + isa = PBXGroup; + children = ( + FDF7FED127A7E2B900E3AB62 /* BaseCoordinator.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; FDF980B92786CE1A0009A14E /* Fonts */ = { isa = PBXGroup; children = ( @@ -1562,6 +1615,7 @@ files = ( FDF980E82786F1910009A14E /* MyPlanVC.swift in Sources */, FDB34C3D27A6D7CE0074B6CE /* ModuleFactory.swift in Sources */, + FDF7FEC927A7E16100E3AB62 /* Coordinator.swift in Sources */, FD1A782D2795EBEF00FB10AB /* PreviewDetailDataGettable.swift in Sources */, FDF980F6278826140009A14E /* TabBarIconView.swift in Sources */, FD09DE0F27857A2E006DA050 /* makeAlert.swift in Sources */, @@ -1577,6 +1631,7 @@ FD3151A5278D0C880076D92E /* PlanDetailVC.swift in Sources */, FD09DE2027857CF2006DA050 /* LoginVC.swift in Sources */, FD2D4DD8278DB054002084B9 /* PlanDetailSummaryFoldTVC.swift in Sources */, + FDF7FEC727A7E15B00E3AB62 /* BaseControllable.swift in Sources */, FD2D9C562796ADC800688A42 /* PlanDetailService.swift in Sources */, FD1A782B2795E63600FB10AB /* PreviewTagDataGettable.swift in Sources */, F266412E2788262800CCC751 /* MainListData.swift in Sources */, @@ -1613,6 +1668,7 @@ FD3211AD27841948007D63D5 /* ViewController.swift in Sources */, FD2D9C602797DF8C00688A42 /* PlanDetailWriterContainerView.swift in Sources */, F2C5F18E2786EACB00BE4036 /* MainCardView.swift in Sources */, + FDF7FEC327A7E14600E3AB62 /* Presentable.swift in Sources */, FD09DE2227857D00006DA050 /* SplashVC.swift in Sources */, FD09DDFC278578B1006DA050 /* ColorLiterals.swift in Sources */, FDF9810A2788D4DD0009A14E /* PlanPreviewRecommendTVC.swift in Sources */, @@ -1634,6 +1690,7 @@ 523C70A32795C3EF0073371D /* TravelSpotDataModel.swift in Sources */, FDF980FF2788C9220009A14E /* PlanPreviewVC.swift in Sources */, FDC47DF12789524000A152E8 /* PreviewSummaryData.swift in Sources */, + FDF7FECF27A7E21600E3AB62 /* Router.swift in Sources */, FD20C620278EF0C900AC6E34 /* MyPlanSettingVC.swift in Sources */, FD20C628278EF6CA00AC6E34 /* BarFullButton.swift in Sources */, FD2D4DB9278D3DCE002084B9 /* PlanDetailInformationTVC.swift in Sources */, @@ -1644,6 +1701,7 @@ FD20C615278ECBE800AC6E34 /* MyPlanEmptyBuyListView.swift in Sources */, FDCE382427993E740042DC54 /* PlanDetailVC + setPreviewDummy.swift in Sources */, 523C70B32796EC890073371D /* ScrapBtnDataModel.swift in Sources */, + FDF7FECC27A7E1DD00E3AB62 /* LaunchInstructor.swift in Sources */, FD20C61C278EEFAB00AC6E34 /* addObserverAction.swift in Sources */, FD2D9C582796B02000688A42 /* PlanDetailVC + fetchData.swift in Sources */, FD3211AB27841948007D63D5 /* SceneDelegate.swift in Sources */, @@ -1671,6 +1729,7 @@ FD09DDFE278578BA006DA050 /* StringLiterals.swift in Sources */, FD1C569127958A9B00EEDFCE /* ResponseObject.swift in Sources */, F294EC2B278EBC66001069F8 /* LoginVC + AppleLogin.swift in Sources */, + FDF7FEC527A7E14F00E3AB62 /* CoordinatorFinishOutput.swift in Sources */, FD09DE2727857DE3006DA050 /* GenericResponse.swift in Sources */, FD09DE1527857B36006DA050 /* getClassName.swift in Sources */, FD20C626278EF6AE00AC6E34 /* BarSelectButton.swift in Sources */, @@ -1681,6 +1740,7 @@ FD2D4DCC278D8636002084B9 /* drawDashedLine + UIView.swift in Sources */, F273216F278BEB7700A9B0CC /* MainCardCarouselLayout.swift in Sources */, FD2D9C5C2797DC4B00688A42 /* PlanDetailMapContainerView.swift in Sources */, + FDF7FED227A7E2B900E3AB62 /* BaseCoordinator.swift in Sources */, FD09DE092785797B006DA050 /* addToolBar.swift in Sources */, FD09DDF527857809006DA050 /* Config.swift in Sources */, 523C70AA2795EC770073371D /* TravelSpotDetailService.swift in Sources */, diff --git a/BeMyPlan/BeMyPlan/Coordinator/Coordinators/BaseCoordinator.swift b/BeMyPlan/BeMyPlan/Coordinator/Coordinators/BaseCoordinator.swift new file mode 100644 index 00000000..47e81686 --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Coordinators/BaseCoordinator.swift @@ -0,0 +1,36 @@ +// +// BaseCoordinator.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +class BaseCoordinator: Coordinator { + + // MARK: - Vars & Lets + var childCoordinators = [Coordinator]() + + // MARK: - Public methods + func addDependency(_ coordinator: Coordinator) { + for element in childCoordinators { + if element === coordinator { return } + } + childCoordinators.append(coordinator) + } + + func removeDependency(_ coordinator: Coordinator?) { + guard childCoordinators.isEmpty == false, let coordinator = coordinator else { return } + + for (index, element) in childCoordinators.enumerated() { + if element === coordinator { + childCoordinators.remove(at: index) + break + } + } + } + + // MARK: - Coordinator + func start() { + start() + } +} diff --git a/BeMyPlan/BeMyPlan/Coordinator/Enums/LaunchInstructor.swift b/BeMyPlan/BeMyPlan/Coordinator/Enums/LaunchInstructor.swift new file mode 100644 index 00000000..934c166c --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Enums/LaunchInstructor.swift @@ -0,0 +1,20 @@ +// +// LaunchInstructor.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +import Foundation + +enum LaunchInstructor { + case signing + case main + + static func configure(_ isAuthorized: Bool = false) -> LaunchInstructor { + switch isAuthorized { + case false: return .signing + case true: return .main + } + } +} diff --git a/BeMyPlan/BeMyPlan/Coordinator/Protocols/BaseControllable.swift b/BeMyPlan/BeMyPlan/Coordinator/Protocols/BaseControllable.swift new file mode 100644 index 00000000..654ef93b --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Protocols/BaseControllable.swift @@ -0,0 +1,10 @@ +// +// BaseControllable.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +import Foundation + +protocol BaseControllable: NSObjectProtocol, Presentable {} diff --git a/BeMyPlan/BeMyPlan/Coordinator/Protocols/Coordinator.swift b/BeMyPlan/BeMyPlan/Coordinator/Protocols/Coordinator.swift new file mode 100644 index 00000000..ec348267 --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Protocols/Coordinator.swift @@ -0,0 +1,12 @@ +// +// Coordinator.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +import Foundation + +protocol Coordinator: AnyObject { + func start() +} diff --git a/BeMyPlan/BeMyPlan/Coordinator/Protocols/CoordinatorFinishOutput.swift b/BeMyPlan/BeMyPlan/Coordinator/Protocols/CoordinatorFinishOutput.swift new file mode 100644 index 00000000..6d570ff0 --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Protocols/CoordinatorFinishOutput.swift @@ -0,0 +1,10 @@ +// +// CoordinatorFinishOutput.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +protocol CoordinatorFinishOutput { + var finishScene: (() -> Void)? { get set } +} diff --git a/BeMyPlan/BeMyPlan/Coordinator/Protocols/Presentable.swift b/BeMyPlan/BeMyPlan/Coordinator/Protocols/Presentable.swift new file mode 100644 index 00000000..b1475121 --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Protocols/Presentable.swift @@ -0,0 +1,18 @@ +// +// Presentable.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +import UIKit + +protocol Presentable { + func toPresent() -> UIViewController? +} + +extension UIViewController: Presentable { + func toPresent() -> UIViewController? { + return self + } +} diff --git a/BeMyPlan/BeMyPlan/Coordinator/Router/Router.swift b/BeMyPlan/BeMyPlan/Coordinator/Router/Router.swift new file mode 100644 index 00000000..0dfb8ee1 --- /dev/null +++ b/BeMyPlan/BeMyPlan/Coordinator/Router/Router.swift @@ -0,0 +1,197 @@ +// +// Router.swift +// BeMyPlan +// +// Created by 송지훈 on 2022/01/31. +// + +import Foundation + +protocol RouterProtocol: Presentable { + + typealias TransiotionType = (CATransitionType, CATransitionSubtype) + + func present(_ module: Presentable?) + func present(_ module: Presentable?, animated: Bool) + func alert(_ module: Presentable?) + + func push(_ module: Presentable?) + func push(_ module: Presentable?, transition: UIViewControllerAnimatedTransitioning?) + func push(_ module: Presentable?, transition: UIViewControllerAnimatedTransitioning?, animated: Bool) + func push(_ module: Presentable?, transition: UIViewControllerAnimatedTransitioning?, animated: Bool, completion: (() -> Void)?) + + func popModule() + func popModule(transition: UIViewControllerAnimatedTransitioning?) + func popModule(transition: UIViewControllerAnimatedTransitioning?, animated: Bool) + + func dismissModule() + func dismissModule(animated: Bool, completion: (() -> Void)?) + + func setRootModule(_ module: Presentable?) + func setRootModule(_ module: Presentable?, hideBar: Bool) + func setRootModule(_ module: Presentable?, type: TransiotionType) + func setRootModule(_ module: Presentable?, type: TransiotionType, hideBar: Bool, animated: Bool) + + func popToRootModule(animated: Bool) + func popToModule(module: Presentable?, animated: Bool) +} + +final class Router: NSObject, RouterProtocol { + + // MARK: - Vars & Lets + + private weak var rootController: UINavigationController? + private var completions: [UIViewController : () -> Void] + private var transition: UIViewControllerAnimatedTransitioning? + + // MARK: - Presentable + + func toPresent() -> UIViewController? { + return self.rootController + } + + // MARK: - RouterProtocol + + func present(_ module: Presentable?) { + present(module, animated: true) + } + + func present(_ module: Presentable?, animated: Bool) { + guard let controller = module?.toPresent() else { return } + controller.modalPresentationStyle = .fullScreen + self.rootController?.present(controller, animated: animated, completion: nil) + } + + func alert(_ module: Presentable?) { + guard let controller = module?.toPresent() else { return } + controller.modalTransitionStyle = .crossDissolve + controller.modalPresentationStyle = .overCurrentContext + self.rootController?.present(controller, animated: true, completion: nil) + } + + func push(_ module: Presentable?) { + self.push(module, transition: nil) + } + + func push(_ module: Presentable?, transition: UIViewControllerAnimatedTransitioning?) { + self.push(module, transition: transition, animated: true) + } + + func push(_ module: Presentable?, transition: UIViewControllerAnimatedTransitioning?, animated: Bool) { + self.push(module, transition: transition, animated: animated, completion: nil) + } + + func push(_ module: Presentable?, transition: UIViewControllerAnimatedTransitioning?, animated: Bool, completion: (() -> Void)?) { + self.transition = transition + guard let controller = module?.toPresent(), + (controller is UINavigationController == false) + else { assertionFailure("Deprecated push UINavigationController."); return } + + if let completion = completion { + self.completions[controller] = completion + } + self.rootController?.pushViewController(controller, animated: animated) + } + + func popModule() { + self.popModule(transition: nil) + } + + func popModule(transition: UIViewControllerAnimatedTransitioning?) { + self.popModule(transition: transition, animated: true) + } + + func popModule(transition: UIViewControllerAnimatedTransitioning?, animated: Bool) { + self.transition = transition + if let controller = rootController?.popViewController(animated: animated) { + self.runCompletion(for: controller) + } + } + + func popToModule(module: Presentable?, animated: Bool) { + if let controllers = self.rootController?.viewControllers , let module = module { + for controller in controllers { + if controller == module as! UIViewController { + self.rootController?.popToViewController(controller, animated: animated) + break + } + } + } + } + + func dismissModule() { + self.dismissModule(animated: true, completion: nil) + } + + func dismissModule(animated: Bool, completion: (() -> Void)?) { + self.rootController?.dismiss(animated: animated, completion: completion) + } + + func setRootModule(_ module: Presentable?) { + self.setRootModule(module, type: (.fade, .fromTop), hideBar: true, animated: true) + } + + func setRootModule(_ module: Presentable?, hideBar: Bool) { + self.setRootModule(module, type: (.fade, .fromTop), hideBar: hideBar, animated: true) + } + + func setRootModule(_ module: Presentable?, type: TransiotionType) { + self.setRootModule(module, type: type, hideBar: true, animated: true) + } + + func setRootModule(_ module: Presentable?, type: TransiotionType, hideBar: Bool, animated: Bool) { + guard let controller = module?.toPresent() else { return } + if animated { + let transition: CATransition = CATransition() + transition.duration = 0.3 + var timingFunction: CAMediaTimingFunction = .init(name: .linear) + if type.0 == .moveIn { + timingFunction = .init(name: .easeOut) + } + if type.0 == .reveal { + timingFunction = .init(name: .easeIn) + } + transition.timingFunction = timingFunction + transition.type = type.0 + transition.subtype = type.1 + self.rootController?.view.layer.add(transition, forKey: nil) + } + self.rootController?.setViewControllers([controller], animated: false) + self.rootController?.navigationBar.isHidden = hideBar + } + + func popToRootModule(animated: Bool) { + self.rootController?.dismiss(animated: false) + if let controllers = self.rootController?.popToRootViewController(animated: animated) { + controllers.forEach { controller in + self.runCompletion(for: controller) + } + } + } + + // MARK: - Private methods + + private func runCompletion(for controller: UIViewController) { + guard let completion = self.completions[controller] else { return } + completion() + completions.removeValue(forKey: controller) + } + + // MARK: - Init methods + + init(rootController: UINavigationController) { + self.rootController = rootController + self.completions = [:] + super.init() +// self.rootController?.delegate = self + } +} + +// MARK: - Extensions +// MARK: - UINavigationControllerDelegate + +extension Router: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return self.transition + } +}