클린아키텍처-클론코딩 (3) MoviesList 페이지 DIContainer와 Coordinator 패턴을 이용해 띄우기

다른 계층이 의존하고 있는 Domain 계층이 완성이 되었으니, 첫 화면인 MovieList 부분부터 진행하도록 해봅시다. 

우선은 MoviesList를 구성해줍니다.

SuggestionView와 MoviesListView를 containerView로 만들어주고 MoviesListView 역시 스토리보드로 UI를 배치해줍니다.

class MoviesListViewController: UIViewController, StoryboardInstatiable, Alertable {

    @IBOutlet var contentView: UIView!
    @IBOutlet var searchBarContainer: UIView!
    @IBOutlet var moviesListContainer: UIView!
    @IBOutlet var suggestionsListContainer: UIView!
    @IBOutlet var emptyDataLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

ViewController에 연결시켜줍니다. 여기서 보통 UI와 ViewController를 연결시킬때 weak로 하지만, 여기서는 Strong으로 연결시켜두었습니다.

moviesListContainer에는 MoviesListTableView와 Cell를 만들어줍니다.

class MoviesListItemCell: UITableViewCell {
    
    static let reuseIdentifier = String(describing: MoviesListItemCell.self)
    static let height = CGFloat(130)
    
    
    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var dateLabel: UILabel!
    @IBOutlet var overviewLabel: UILabel!
    @IBOutlet var posterImageView: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

    }

}
import UIKit

class MoviesListTableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 0
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }

}

이제 시뮬레이터를 실행시켰을때 첫 화면에 MoviesListView가 나타나도록 해봅시다. 여기서는 Coordinator 패턴을 사용하여 View생성을 관리해주고 있습니다.

우선은 AppCoordinator를 만들어 줍니다.

protocol Coordinator {
    func start()
}

final class AppFlowCoordinator: Coordinator {
    
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
    }
    
}

그리고 AppDelegate에서 appCoordinator을 생성하고 navigationController역시 생성하여 주입해줍니다.

    var appCoordinator: AppFlowCoordinator?
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let navigationController = UINavigationController()
        appCoordinator = AppFlowCoordinator(navigationController: navigationController)
        appCoordinator?.start()
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
        
        return true
    }

여기서 이제 appFlowCoordinator에서 moviesSearchFlowCoordinator를 자식 coordinator로 가지고 start로 실행, navigaionController에 push하는 형태로 화면이 띄어지게 됩니다. 

여기서 AppFlowCoordinator는 MoviesSearchFlowCoordinator를 의존하게 됩니다. 즉 MoviesSearchFlowCoordinator 객체를 가지고 있어야 합니다. MoviesSearchFlowCoordinator도 MoviesListViewController를 가지고 있어야 합니다. ViewController 역시 ViewModel이나 Repository 객체를 보유하고 있어야 합니다. 

이렇게 의존성을 요구하는 코드가 많아지고 복잡해지면, 생명주기 관리에도 어려움이 있습니다. 여기서 하나의 객체에서 의존성을 관리를 해 줄수 있게 하는데 이게 DIContainer입니다.

MoviesSearchFlowCoordinator까지의 코드를 도식화 해보면 의존성 있는 코드는 모두 DIContainer에서 주입해주는것을 볼 수 있습니다.

우선은 AppDIContainer부터 만들어봅시다.

final class AppDIContainer {
    lazy var appConfiguration = AppCongifuraton()
    
    func makeMoviesSceneDIContainer() -> MoviesSceneDIContainer {
        return MoviesSceneDIContainer()
    }
}

보통 객체를 만들어서 DIContainer에 register(등록), resolve(사용)하는 방식으로 하는데 여기서는 make함수를 이용하여 객체를 생성, 리턴 해주고 있습니다. 그리고 AppDelegate에 AppDIContainer를 생성해주고, AppFlowCoordinator에 주입, start하면 moviesSceneDIContainer가 moviesSearchFlowCoordinator를 생성하도록 해줍니다.

final class AppFlowCoordinator {

    var navigationController: UINavigationController
    private let appDIContainer: AppDIContainer
    
    init(navigationController: UINavigationController,
         appDIContainer: AppDIContainer) {
        self.navigationController = navigationController
        self.appDIContainer = appDIContainer
    }

    func start() {
        // In App Flow we can check if user needs to login, if yes we would run login flow
        let moviesSceneDIContainer = appDIContainer.makeMoviesSceneDIContainer()
        let flow = moviesSceneDIContainer.makeMoviesSearchFlowCoordinator(navigationController: navigationController)
        flow.start()
    }
}
//AppDelegate
let appDIContainer = AppDIContainer()
    var appFlowCoordinator: AppFlowCoordinator?
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        AppAppearance.setupAppearance()
        
        window = UIWindow(frame: UIScreen.main.bounds)
        let navigationController = UINavigationController()

        window?.rootViewController = navigationController
        appFlowCoordinator = AppFlowCoordinator(navigationController: navigationController,
                                                appDIContainer: appDIContainer)
        appFlowCoordinator?.start()
        window?.makeKeyAndVisible()
    
        return true
    }
final class MoviesSceneDIContainer {
    
    func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
        return MoviesSearchFlowCoordinator(navigationController: navigationController, dependencies: self)
    }
}
final class MoviesSearchFlowCoordinator: Coordinator {
    private var navigationController: UINavigationController?
    
    private weak var moviesListVC: MoviesListViewController?
    private weak var moviesQueriesSuggestionsVC: UIViewController?
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let vc = MoviesListViewController.instantiateViewController()
        self.navigationController?.pushViewController(vc, animated: false)
    }
}

이제 실행을 해보면 Coordinator와 DIContainer가 의도한대로 작동한것을 볼 수 있습니다.