-
[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๋ฅผ ์์ฑํ๊ณCoordinatorprotocol์ ์ฑํํ๊ฒ ์ต๋๋ค. ์ฐ๋ฆฌ๋ ๋ํ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 } }๊ฒฐ๊ณผ์ ์ผ๋ก, ๋ค์๊ณผ ๊ฐ์ ๋ ์ด์์์ ๋ณผ ์ ์์ต๋๋ค.

์ฌ์ฉ์๊ฐ
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๋ฅผ ๊ฐ์ง๋๋ค.

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 } }์ด์ ๋ค์๊ณผ ๊ฐ์ ๋ ์ด์์์ ๋ณผ ์ ์์ต๋๋ค.

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๋ฅผ ๊ตฌ์ฑํ์์ต๋๋ค.

The Leaks Instrument
๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ ์ด๋จ๊น์?
Leaks Instrument๋ฅผ ํตํด ๊ฐ ํด์ ๋ ๋ทฐ์ปจํธ๋กค๋ฌ์ ํด๋น coordinator๊ฐ ํ ๋น ํด์ ๋๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
'iOS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[iOS] Command CompileSwiftSources failed with a nonzero exit code ์ค๋ฅ ํด๊ฒฐ (0) 2022.07.08 [iOS] ํ์ ์๋ฆผ์ฐฝ ๊ตฌํ ์ค ๋ฐ์ํ Cannot be called with asCopy = NO on non-main thread. ๋ฌธ์ ํด๊ฒฐ (0) 2022.07.07 [iOS] safeAreaLayoutGuide (0) 2022.05.26 [iOS] SwiftLint ์ ์ฉํ๊ธฐ (0) 2022.05.10 [iOS] SnapKit ์ฌ์ฉํ๊ธฐ (0) 2022.05.10