ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS] Coordinator Design Pattern ๋ฒˆ์—ญ
    iOS 2022. 5. 26. 13:03

    Coordinator Design Pattern ๋ฒˆ์—ญ

    Coordinator Design Pattern์„ ์‚ฌ์šฉํ•˜๋ฉด, ์•ฑ์˜ ํ๋ฆ„์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ณ  ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋กœ์ง์„ ์ปจํŠธ๋กค๋Ÿฌ์— ์ง์ ‘ ๋„ฃ๋Š” ๊ฒƒ์„ ํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์„œ๋กœ ๋ถ„๋ฆฌํ•˜๋Š”๋ฐ ๋„์›€์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

    ๋‹ค์Œ ๋ถ€๋ถ„์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    • ๊ฐœ๋ณ„ ์ฝ”๋””๋„ค์ดํ„ฐ ์ƒ์„ฑ
    • ์ฝ”๋””๋„ค์ดํ„ฐ ํ๋ฆ„ ํ™œ์šฉ
    • ์ฝ”๋””๋„ค์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ ๊ฐ„์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ

    ํ”„๋กœ์ ํŠธ์˜ ์†Œ์Šค ์ฝ”๋“œ๋Š” ํ•˜๋‹จ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์‹œ์ž‘ํ•˜๊ธฐ

    ๋จผ์ € ๋ชจ๋“  Coordinator๊ฐ€ ์ค€์ˆ˜ํ•  ํ”„๋กœํ† ์ฝœ Coordinator๋ฅผ ์ƒ์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    import UIKit
    
    protocol Coordinator {
        func start()
        func coordinate(to coordinator: Coordinator)
    }
    
    extension Coordinator {
        func coordinate(to coordinator: Coordinator) {
            coordinator.start()
        }
    }

    ์ด์ œ ์•ฑ ์‹œ์ž‘์„ ๋‹ด๋‹นํ•  AppCoordinator๋ฅผ ๋งŒ๋“ค์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    class AppCoordinator: Coordinator {
    
        let window: UIWindow
    
        init(window: UIWindow) {
            self.window = window
        }
    
        func start() {
            let navigationController = UINavigationController()
            window.rootViewController = navigationController
            window.makeKeyAndVisible()
    
            let startCoordinator = StartCoordinator(navigationController: navigationController)
            coordinate(to: startCoordinator)
        }
    }

    ์ดˆ๊ธฐ ์„ค์ •์„ ์™„๋ฃŒํ•˜๋ ค๋ฉด AppDelegate์—์„œ AppCoordinator์˜ start ๋ฉ”์†Œ๋“œ๋ฅผ ์‹คํ–‰ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    @main
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        var window: UIWindow?
        var coordinator: AppCoordinator?
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            window = UIWindow()
    
            coordinator = AppCoordinator(window: window!)
            coordinator?.start()
            return true
        }
    }

    ์ด์ œ ์•ฑ์˜ ์ฒซ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    StartCoordinator

    AppCoordinator์—์„œ ํ–ˆ๋˜ ๊ฒƒ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ StartCoordinator๋ฅผ ์ƒ์„ฑํ•˜๊ณ  Coordinator protocol์„ ์ฑ„ํƒํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๋˜ํ•œ coordinateToTabBar() ๋ฉ”์†Œ๋“œ๊ฐ€ ํฌํ•จ๋œ StartFlow๋ผ๋Š” ํ”„๋กœํ† ์ฝœ์„ ์ •์˜ํ•˜๊ณ , ์ด๋ฅผ StartViewController์—์„œ ์‹คํ–‰์‹œํ‚ฌ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    import UIKit
    
    protocol StartFlow: AnyObject {
        func coordinateToTabBar()
    }
    
    class StartCoordinator: Coordinator {
        let navigationController: UINavigationController
    
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
    
        func start() {
            let startViewController = StartViewController()
            startViewController.coordinator = self
            navigationController.pushViewController(startViewController, animated: true)
        }
    
        // MARK: - Flow Methods
        func coordinateToTabBar() {
            let tabBarCoordinator = TabBarCoordinator(navigationController: navigationController)
            coordinate(to: tabBarCoordinator)
        }
    }

    StartViewController

    ์—ฌ๊ธฐ์„œ๋Š” ๋‹จ์ˆœํžˆ ํ™”๋ฉด ์ค‘์•™์— ๋ฒ„ํŠผ์„ ๋ฐฐ์น˜ํ•˜๊ณ , startTapped ํ•ธ๋“ค๋Ÿฌ (coordinateToTabBar() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    class StartViewController: UIViewController {
    
        // MARK: - Lifycycle
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        // MARK: - Actions
        @objc func startTapped(_ sender: UIButton) {
            coordinator?.coordinateToTabBar()
        }
    
        // MARK: - Properties
        var coordinator: StartFlow?
    
        let startButton: UIButton = {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            button.setTitle("Start!", for: .normal)
            button.setTitleColor(.white, for: .normal)
            button.backgroundColor = .systemBlue
            button.layer.cornerRadius = 10
            button.layer.shadowRadius = 5
            button.layer.shadowColor = UIColor.systemTeal.cgColor
            button.layer.shadowOpacity = 1.0
            button.layer.shadowOffset = CGSize(width: -1, height: 3)
            button.addTarget(StartViewController.self, action: #selector(startTapped(_:)), for: .touchUpInside)
            return button
        }()
    }
    
    // MARK: - UI Setup
    extension StartViewController {
        private func setupUI() {
            if #available(iOS 13.0, *) {
                overrideUserInterfaceStyle = .light
            }
    
            self.view.backgroundColor = .white
            self.view.addSubview(startButton)
    
            startButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 3).isActive = true
            startButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
            startButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            startButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        }
    }

    TabBarCoordinator์— coordinate ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    (์•„๋ž˜ ์ฝ”๋“œ๋Š” StartCoordinator.swift์— ์žˆ์Šต๋‹ˆ๋‹ค.)

    // MARK: - Flow Methods
    func coordinateToTabBar() {
        let tabBarCoordinator = TabBarCoordinator(navigationController: navigationController)
        coordinate(to: tabBarCoordinator)
    }

    coordinator๋ฅผ ์ธ์Šคํ„ด์Šคํ™”ํ•˜๊ณ  coordinate(to:) ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

    ์ด์ œ coordinator๋ฅผ UITabBarController์—์„œ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    TabBarController

    import UIKit
    
    class TabBarCoordinator: Coordinator {
        let navigationController: UINavigationController
    
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
    
        func start() {
            let tabBarController = TabBarController()
            tabBarController.coordinator = self
    
            let topRatedNavigationController = UINavigationController()
            topRatedNavigationController.tabBarItem = UITabBarItem(tabBarSystemItem: .topRated, tag: 0)
            let topRatedCoordinator = TopRatedCoordinator(navigationController: topRatedNavigationController)
    
            let searchNavigationController = UINavigationController()
            searchNavigationController.tabBarItem = UITabBarItem(tabBarSystemItem: .search, tag: 1)
            let searchCoordinator = SearchCoordinator(navigationController: searchNavigationController)
    
            let historyNavigationController = UINavigationController()
            historyNavigationController.tabBarItem = UITabBarItem(tabBarSystemItem: .history, tag: 2)
            let historyCoordinator = HistoryCoordinator(navigationController: historyNavigationController)
    
            tabBarController.viewControllers = [topRatedNavigationController,
                                                searchNavigationController,
                                                historyNavigationController]
    
            tabBarController.modalPresentationStyle = .fullScreen
            navigationController.present(tabBarController, animated: true)
    
            coordinate(to: topRatedCoordinator)
            coordinate(to: searchCoordinator)
            coordinate(to: historyCoordinator)
        }
    }

    start() ๋ฉ”์†Œ๋“œ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด, ์šฐ๋ฆฌ๋Š” ๊ฐœ๋ณ„ UINavigationController๋“ค๊ณผ coordinator๋“ค์„ ์ƒ์„ฑํ•˜๊ณ , ์ดํ›„์— ๊ฐ๊ฐ์˜ coordinator์—์„œ coordinate(to:) ๋ฉ”์†Œ๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

    ๊ฒฐ๊ณผ์ ์œผ๋กœ, ์šฐ๋ฆฌ๋Š” ๋‹ค๋ฅธ ์ปจํŠธ๋กค๋Ÿฌ์— ๋Œ€ํ•ด ์•Œ์ง€ ๋ชปํ•˜๋Š” TabBarController ํด๋ž˜์Šค๋ฅผ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    class TabBarCoordinator: Coordinator {
        let navigationController: UINavigationController
    
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
    
        func start() {
            let tabBarController = TabBarController()
            tabBarController.coordinator = self
    
            let topRatedNavigationController = UINavigationController()
            topRatedNavigationController.tabBarItem = UITabBarItem(tabBarSystemItem: .topRated, tag: 0)
            let topRatedCoordinator = TopRatedCoordinator(navigationController: topRatedNavigationController)
    
            let searchNavigationController = UINavigationController()
            searchNavigationController.tabBarItem = UITabBarItem(tabBarSystemItem: .search, tag: 1)
            let searchCoordinator = SearchCoordinator(navigationController: searchNavigationController)
    
            let historyNavigationController = UINavigationController()
            historyNavigationController.tabBarItem = UITabBarItem(tabBarSystemItem: .history, tag: 2)
            let historyCoordinator = HistoryCoordinator(navigationController: historyNavigationController)
    
            tabBarController.viewControllers = [topRatedNavigationController,
                                                searchNavigationController,
                                                historyNavigationController]
    
            tabBarController.modalPresentationStyle = .fullScreen
            navigationController.present(tabBarController, animated: true)
    
            coordinate(to: topRatedCoordinator)
            coordinate(to: searchCoordinator)
            coordinate(to: historyCoordinator)
        }
    }

    ํƒญ ๋ฐ”์˜ ๊ฐœ๋ณ„ ์Šคํฌ๋ฆฐ๋“ค: TopRated, Search, History๋ฅผ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    TopRatedCoordinator

    TopRatedViewController์˜ coordinator๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    protocol TopRatedFlow: AnyObject {
        func coordinateToDetail()
    }
    
    class TopRatedCoordinator: Coordinator, TopRatedFlow {
        weak var navigationController: UINavigationController?
    
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
    
        func start() {
            let topRatedViewController = TopRatedViewController()
            topRatedViewController.coordinator = self
    
            navigationController?.pushViewController(topRatedViewController, animated: false)
        }
    
        // MARK: - Flow Methods
        func coordinateToDetail() {
            let topRatedDetailCoordinator = TopRatedDetailCoordinator(navigationController: navigationController)
            coordinate(to: topRatedDetailCoordinator)
        }
    }

    ๊ทธ๋ฆฌ๊ณ  TopRatedViewController๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    TopRatedViewController

    import UIKit
    
    class TopRatedViewController: UIViewController {
    
        // MARK: - Lifecycle
        override func viewDidLoad() {
            super.viewDidLoad()
    
            setupUI()
        }
    
        // MARK: - Actions
        @objc func showDetailTapped(_ sender: UIButton) {
            coordinator?.coordinateToDetail()
        }
    
        // MARK: - Properties
        var coordinator: TopRatedFlow?
    
        let showDetailButton: UIButton = {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            button.setTitle("Show detail", for: .normal)
            button.setTitleColor(.white, for: .normal)
            button.backgroundColor = UIColor.systemOrange
            button.layer.cornerRadius = 10
            button.layer.shadowRadius = 5
            button.layer.shadowColor = UIColor.orange.cgColor
            button.layer.shadowOpacity = 1.0
            button.layer.shadowOffset = CGSize(width: -1, height: 3)
            button.addTarget(TopRatedViewController.self, action: #selector(showDetailTapped), for: .touchUpInside)
            return button
        }()
    }
    
    // MARK: - UI Setup
    extension TopRatedViewController {
        private func setupUI() {
            if #available(iOS 13.0, *) {
                overrideUserInterfaceStyle = .light
            }
    
            self.view.backgroundColor = .white
            self.view.addSubview(showDetailButton)
    
            showDetailButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 3).isActive = true
            showDetailButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
            showDetailButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            showDetailButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        }
    }

    ๊ฒฐ๊ณผ์ ์œผ๋กœ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ ˆ์ด์•„์›ƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    1_nlceBqeF09ksoe_8O2S-Mw.png

    ์‚ฌ์šฉ์ž๊ฐ€ showDetailButton์„ ํƒญํ•  ๋•Œ ํ‘œ์‹œํ•  ์„ธ๋ถ€ ์ •๋ณด ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    TopRatedDetailCoordinator

    ์—ฌ๊ธฐ์—์„œ๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ๊ฒƒ์ด ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ์ฐจ์ด์ ์€ pushViewController ๋Œ€์‹  present ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    import UIKit
    
    protocol TopRatedDetailFlow {
        func dismissDetail()
    }
    
    class TopRatedDetailCoordinator: Coordinator, TopRatedDetailFlow {
        let navigationController: UINavigationController
    
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
    
        func start() {
            let topRatedDetailViewController = TopRatedDetailViewController()
            topRatedDetailViewController.coordinator = self
    
            navigationController.present(topRatedDetailViewController, animated: true)
        }
    
        // MARK: - Flow Methods
        func dismissDetail() {
            navigationController.dismiss(animated: true)
        }
    }

    TopRatedDetailViewController

    ์ด์ „ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์ฒ˜๋Ÿผ, dimissDetailTapped ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” coordinator ํ”„๋กœํผํ‹ฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

    import UIKit
    
    class TopRatedDetailViewController: UIViewController {
    
        // MARK: - Lifecycle
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
        }
    
        // MARK: - Actions
        @objc func dismissDetailTapped(_ sender: UIButton) {
            coordinator?.dismissDetail()
        }
    
        // MARK: - Properties
        var coordinator: TopRatedDetailFlow?
    
        let dismissDetailButton: UIButton = {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            button.setTitle("Dismiss detail", for: .normal)
            button.setTitleColor(.white, for: .normal)
            button.backgroundColor = UIColor.systemGray
            button.layer.cornerRadius = 10
            button.layer.shadowRadius = 5
            button.layer.shadowColor = UIColor.gray.cgColor
            button.layer.shadowOpacity = 1.0
            button.layer.shadowOffset = CGSize(width: -1, height: 3)
            button.addTarget(TopRatedDetailViewController.self, action: #selector(dismissDetailTapped), for: .touchUpInside)
            return button
        }()
    
    }
    
    // MARK: - UI Setup
    extension TopRatedDetailViewController {
        private func setupUI() {
            if #available(iOS 13.0, *) {
                overrideUserInterfaceStyle = .light
            }
            self.view.backgroundColor = .white
            self.view.addSubview(dismissDetailButton)
    
            dismissDetailButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 3).isActive = true
            dismissDetailButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
            dismissDetailButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            dismissDetailButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        }
    }

    ๊ฒฐ๊ณผ์ ์œผ๋กœ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ workflow๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

    1_Ky9EsbOAbZf0Qy4QMW0XQg.gif

    Search์˜ workflow๋„ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  History ํ™”๋ฉด์—์„œ coordinator ๊ฐ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ์ง‘์ค‘ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    HistoryItem

    ์ด ๋ชจ๋ธ์€ ํ•˜๋‚˜์˜ coordinator์—์„œ ๋‹ค๋ฅธ coordinator๋กœ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ๋งค์šฐ ๊ฐ„๋‹จํ•œ ํด๋ž˜์Šค Model์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

    import Foundation
    
    struct HistoryItem {
        let title: String
    }

    HistoryCoordinator

    ์ด์ „๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ start() ๋ฉ”์†Œ๋“œ์˜ navigation ์Šคํƒ์— ์ƒˆ๋กœ์šด ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ธ์Šคํ„ด์Šคํ™”ํ•˜์—ฌ ํ‘ธ์‹œํ•ฉ๋‹ˆ๋‹ค.

    import Foundation
    import UIKit
    
    protocol HistoryFlow {
        func coordinateToDetail(with title: String)
    }
    
    class HistoryCoordinator: Coordinator, HistoryFlow {
        weak var navigationController: UINavigationController?
    
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
    
        func start() {
            let historyViewController = HistoryViewController()
            historyViewController.coordinator = self
    
            navigationController?.pushViewController(historyViewController, animated: false)
        }
    
        // MARK: - Flow Methods
        func coordinateToDetail(with title: String) {
            let historyDetailCoordinator = HistoryDetailCoordinator(navigationController: navigationController!, historyItemTitle: title)
    
            coordinate(to: historyDetailCoordinator)
        }
    }import Foundation
    
    struct HistoryItem {
        let title: String
    }

    HistoryViewController

    HistoryItem ๊ฐ์ฒด๋ฅผ ํฌํ•จํ•œ UITableView๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • UITableViewCell์„ ์„ ๋‚ตํ•˜๋ฉด, didSelectRow ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. (์ด์— ๋Œ€์‘ํ•˜๋Š” HistoryItem์„ ์บก์ณํ•ด์„œ HistoryCoordinator์—๊ฒŒ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.)

    import UIKit
    
    class HistoryViewController: UIViewController {
    
        // MARK: - Lifecycle
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
        }
    
        // MARK: - Properties
        var coordinator: HistoryFlow?
    
        let historyItems: [HistoryItem] = [.init(title: "First item"),
                                           .init(title: "Second item"),
                                           .init(title: "Third item"),
                                           .init(title: "Fourth item")]
    
        lazy var tableView: UITableView = {
            let tableView = UITableView()
            tableView.translatesAutoresizingMaskIntoConstraints = false
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
            tableView.delegate = self
            tableView.dataSource = self
            return tableView
        }()
    }
    
    // MARK: - UITableView Delegate & Data Source
    extension HistoryViewController: UITableViewDelegate, UITableViewDataSource {
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return historyItems.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
            cell.textLabel?.text = historyItems[indexPath.row].title
            return cell
        }
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            coordinator?.coordinateToDetail(with: historyItems[indexPath.row].title)
            tableView.deselectRow(at: indexPath, animated: true)
        }
    }
    
    // MARK: - UI Setup
    extension HistoryViewController {
        private func setupUI() {
            if #available(iOS 13.0, *) {
                overrideUserInterfaceStyle = .light
            }
    
            self.view.backgroundColor = .white
            self.view.addSubview(tableView)
    
            tableView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
            tableView.heightAnchor.constraint(equalTo: self.view.heightAnchor).isActive = true
        }
    }

    ์ด์ œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ ˆ์ด์•„์›ƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    1_MIFGODLLrR0c-VKaQOVufA.png

    detail screen์œผ๋กœ ์ด๋™ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    HistoryDetailCoordinator

    ์šฐ๋ฆฌ๋Š” ํ‘œ์ค€ navigationController ์™ธ์— historyItemTitle ํ”„๋กœํผํ‹ฐ๋“ค๋„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    HistoryDetailViewController์˜ ๊ณ ์œ ํ•œ historyItemTitle ํ”„๋กœํผํ‹ฐ๋ฅผ start() ๋ฉ”์†Œ๋“œ์— ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    protocol HistoryDetailFlow {
        func dismissDetail()
    }
    
    class HistoryDetailCoordinator: Coordinator, HistoryDetailFlow {
        let navigationController: UINavigationController
        let historyItemTitle: String
    
        init(navigationController: UINavigationController,
             historyItemTitle: String) {
    
            self.navigationController = navigationController
            self.historyItemTitle = historyItemTitle
        }
    
        func start() {
            let historyDetailViewController = HistoryDetailViewController()
            historyDetailViewController.historyItemTitle = historyItemTitle
            historyDetailViewController.coordinator = self
    
            navigationController.present(historyDetailViewController, animated: true, completion: nil)
        }
    
        // MARK: - Flow Methods
        func dismissDetail() {
            navigationController.dismiss(animated: true, completion: nil)
        }
    }

    HistoryDetailViewController

    ์ด ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋‹จ์ˆœํžˆ ํ™”๋ฉด ์ƒ๋‹จ์— ๋ ˆ์ด๋ธ”์„ ํ‘œ์‹œํ•˜๊ณ , ์ค‘์•™์— dismiss ๋ฒ„ํŠผ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

    import UIKit
    
    class HistoryDetailViewController: UIViewController {
    
        // MARK: - Lifecycle
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
        }
    
        // MARK: - Actions
        @objc func dismissDetailTapped(_ sender: UIButton) {
            coordinator?.dismissDetail()
        }
    
        // MARK: - Properties
        var coordinator: HistoryDetailFlow?
        var historyItemTitle: String? {
            didSet {
                self.titleLabel.text = historyItemTitle
            }
        }
    
        let titleLabel: UILabel = {
            let label = UILabel()
            label.textColor = .systemIndigo
            label.translatesAutoresizingMaskIntoConstraints = false
            return label
        }()
    
        let dismissDetailButton: UIButton = {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            button.setTitle("Dismiss detail", for: .normal)
            button.setTitleColor(.white, for: .normal)
            button.backgroundColor = UIColor.systemGray
            button.layer.cornerRadius = 10
            button.layer.shadowRadius = 5
            button.layer.shadowColor = UIColor.gray.cgColor
            button.layer.shadowOpacity = 1.0
            button.layer.shadowOffset = CGSize(width: -1, height: 3)
            button.addTarget(HistoryDetailViewController.self, action: #selector(dismissDetailTapped), for: .touchUpInside)
            return button
        }()
    
    }
    
    // MARK: - UI Setup
    extension HistoryDetailViewController {
        private func setupUI() {
            if #available(iOS 13.0, *) {
                overrideUserInterfaceStyle = .light
            }
            self.view.backgroundColor = .white
            self.view.addSubview(titleLabel)
            self.view.addSubview(dismissDetailButton)
    
            titleLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            titleLabel.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20).isActive = true
    
            dismissDetailButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 3).isActive = true
            dismissDetailButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
            dismissDetailButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            dismissDetailButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        }
    }

    ๋‹ค์Œ๊ณผ ๊ฐ™์€ workflow๋ฅผ ๊ตฌ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    1_DYtHum1YPhMOeQMPecmvUg.gif

    The Leaks Instrument

    ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋Š” ์–ด๋–จ๊นŒ์š”?

    Leaks Instrument๋ฅผ ํ†ตํ•ด ๊ฐ ํ•ด์ œ๋œ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์™€ ํ•ด๋‹น coordinator๊ฐ€ ํ• ๋‹น ํ•ด์ œ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๋Œ“๊ธ€

Designed by Tistory.