클린아키텍처-클론코딩 (4) 영화 검색-1

이번에는 검색창에 검색어를 입력해서 값을 받아오는 작업을 진행하도록 해봅니다.

우선은 검색에 필요한 ViewModel의 Input과 Output을 정의합니다.

protocol MoviesListViewModelInput {
    func viewDidLoad()
    func didSearch(query: String)
}

protocol MoviesListViewModelOutput {
    var items: Observable<[MoviesListItemViewModel]> {get}
    var query: Observable<String> {get}
    var error: Observable<String> {get}
    var isEmpty: Bool {get}
    var screenTitle: String {get}
    var emptyDataTitle: String {get}
    var errorTitle: String {get}
    var searchbarPlaceHolder: String {get}
}

protocol MoviesListViewModel: MoviesListViewModelInput, MoviesListViewModelOutput {}

그리고 이를 채택한 DefaultMoviesListViewModel을 만들어줍니다.

final class DefaultMoviesListViewModel: MoviesListViewModel {
    var items: Observable<[MoviesListItemViewModel]> = Observable([])
    var query: Observable<String> = Observable("")
    var error: Observable<String> = Observable("")
    var isEmpty: Bool { return items.value.isEmpty }
    var screenTitle: String = "Movies"
    var emptyDataTitle: String = "Search Results"
    var errorTitle: String = "Error"
    var searchbarPlaceHolder: String = "Search Movies"
}

extension DefaultMoviesListViewModel {
    func viewDidLoad() {
        
    }
    
    func didSearch(query: String) {
        
    }
}

 

이제 MoviesSearchFlowCoordinator에 DIContainer를 이용해 MoviesListViewController에 ViewModel을 주입시켜줍니다. MoviesSearchFlowCoordinator역시 ViewController를 DIContainer에서 가져오므로 MoviesSceneDIContainer에 ViewModel과 ViewController를 반환해주는 함수를 만들어줍니다. 

protocol MoviesSearchFlowCoordinatorDependencies {
    func makeMoviesListViewController() -> MoviesListViewController
}
final class MoviesSearchFlowCoordinator: Coordinator {
    private var navigationController: UINavigationController?
    private let dependencies: MoviesSearchFlowCoordinatorDependencies
    
    private weak var moviesListVC: MoviesListViewController?
    
    init(navigationController: UINavigationController, dependencies: MoviesSearchFlowCoordinatorDependencies) {
        self.navigationController = navigationController
        self.dependencies = dependencies
    }
    
    func start() {
        let vc = dependencies.makeMoviesListViewController()
        self.navigationController?.pushViewController(vc, animated: false)
    }
    
}
final class MoviesSceneDIContainer {
    
    func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
        return MoviesSearchFlowCoordinator(navigationController: navigationController, dependencies: self)
    }
   
    func makeMoviesListViewModel() -> MoviesListViewModel {
        return DefaultMoviesListViewModel()
    }
}

extension MoviesSceneDIContainer: MoviesSearchFlowCoordinatorDependencies {
    func makeMoviesListViewController() -> MoviesListViewController {
        return MoviesListViewController.create(with: makeMoviesListViewModel())
    }
    
}

이제 ViewController에서 SearchBar와 타이틀 및 글자들을 삽입해줍니다.

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!
    
    private var viewModel: MoviesListViewModel!
    
    private var searchController = UISearchController(searchResultsController: nil)
    
    static func create(with viewModel: MoviesListViewModel) -> MoviesListViewController{
        let view = MoviesListViewController.instantiateViewController()
        view.viewModel = viewModel
        return view
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        moviesListContainer.isHidden = true
    }

    private func setupView() {
        title = viewModel?.screenTitle
        emptyDataLabel.text = viewModel?.emptyDataTitle
        setupSearchController()
    }
    
}

extension MoviesListViewController {
    private func setupSearchController() {
        searchController.delegate = self
        searchController.searchBar.delegate = self
        searchController.searchBar.placeholder = viewModel.searchbarPlaceHolder
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.translatesAutoresizingMaskIntoConstraints = true
        searchController.searchBar.barStyle = .black
        searchController.hidesNavigationBarDuringPresentation = false
        searchController.searchBar.frame = searchBarContainer.bounds
        searchController.searchBar.autoresizingMask = [.flexibleWidth]
        searchBarContainer.addSubview(searchController.searchBar)
    }
}

extension MoviesListViewController: UISearchBarDelegate {
    
}

extension MoviesListViewController: UISearchControllerDelegate {
    
}

이제 기본적인 UI와 ViewModel, ViewController가 갖춰졌으니 검색을 해서 영화 리스트를 받아오는 기능을 구현해봅시다.

 

SearchBar에서 입력받은 text를 viewModel의 input인 didSearch를 이용하여 검색을 수행합니다.

extension MoviesListViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let searchText = searchBar.text, !searchText.isEmpty else {return}
        viewModel.didSearch(query: searchText)
    }
}
    func didSearch(query: String) {
        guard !query.isEmpty else { return }
        update(movieQuery: MovieQuery(query: query))
    }
private func update(movieQuery: MovieQuery) {
        load(movieQuery: movieQuery, loading: .fullScreen)
    }
private func load(movieQuery: MovieQuery, loading: MoviesListViewModelLoading) {
        moviesLoadTask = searchMoviesUseCase.excute(requestValue: .init(query: movieQuery, page: nextPage), cached: appendPage(_:), completion: { result in
            switch result {
            case .success(let page):
                print(page)
            case .failure(let error):
                print(error.localizedDescription)
            }
        })
    }

코드를 보면 didSearch에서 searchMoviesUseCase를 바로 실행하는게 아닌 update -> load 를 통하여 실행하는것을 볼 수 있습니다.  이런식으로 코드를 구성한 이유는 제가 생각했을때 가독성때문이 아닌가 생각합니다. 로버트 C 마틴의 클린 코드의 내용중에서 가독성이 좋은 코드는 서로 추상화 수준이 같은 코드끼리 모여 있으며, 함수는 하나의 추상화된 작업만 진행해야 한다고 하고 있습니다. 따라서 didSearch(검색) 이라는 추상화된 작업과 지금은 코드에는 없지만 update에 페이지를 리셋하는 함수와 쿼리문을 가지고 검색을 수행하는 함수 같이 같은 수준의 추상화된 함수를 같은 함수에 넣음으로서 보다 가독성이 좋도록 하기 위해 이런식으로 코드를 구성한 것이라고 생각이 드네요.

 

아무튼 이제 ViewModel에 UseCase를 주입하도록 합니다. 

생성자에 만들어 준후 DIContainer의 makeMoviesListViewModel을 수정하고 UseCase를 만들어주는 함수를 추가합니다. 

init(searchMoviesUseCase: SearchMoviesUseCase) {
        self.searchMoviesUseCase = searchMoviesUseCase
    }
func makeSearchMoviesUseCase() -> SearchMoviesUseCase {
        return DefaultSearchMoviesUseCase(moviesRepository: <#T##MoviesRepository#>, moviesQueriesRepository: <#T##MoviesQueriesRepository#>)
    }

이제 Repository를 구현해 보도록 합시다.