Coordinator 패턴이란 ViewController의 화면 전환의 책임을 Coordinator에게 맡기는 것이다.
Coordinator를 통해 의존성을 주입하고 각 VC 간의 화면 전환을 함으로써 VC는 UI와 사용자와의 이벤트 상호작용에만 집중할 수 있다.
이전에 과제와 프로젝트를 진행하며 현재 표시화면이 아닌 다른 VC의 함수를 호출한다거나, 화면 전환 시 공통으로 사용하는 데이터를 넘겨주는 과정에서 어려움이 있었는데, Coordinator 패턴을 통해 이러한 어려움을 해소할 수 있을 것 같아 이번 과제에 적용해보았다.
final class ResultViewController: UIViewController {
...
private func bind(searchController: UISearchController) {
let input = SeachViewModel.Input(
searchText: searchController.searchBar.rx.text.orEmpty
.throttle(.milliseconds(500), scheduler: MainScheduler.instance)
.asObservable()
)
let output = viewModel.transform(input)
output.tvShow
.subscribe(onNext: {
// 처리
}, onError: {
// 처리
})
.disposed(by: disposeBag)
}
}
searchController에서 검색 시 throttle을 사용하여 지정한 시간이 지나면 마지막 text값을 가지고 검색하는 기능을 구현하였다.

예상한 결과는 throttle 시간이 종료되고 마지막 값을 1번만 방출하는 것이었는데, 텍스트를 입력한 수 만큼 이벤트가 방출되고 있었다.
검색 결과를 보여주는 ResultViewController는 메인 화면인 HomeViewController에서 UISearchController.searchResultsController로 사용되고 있다.
Coordinator 패턴을 활용하여 AppCoordinator에서 VC를 생성하여 주입하고 있는데, 그때문에 ResultVC에서는 SearchBar에 접근이 불가했다.
class AppCoordinator: Coordinator {
...
func start() {
let vc = HomeViewController(viewModel: MusicViewModel(networkService: networkService))
vc.coordinator = self
setupSearchController(for: vc) // searchController 설정
navigationController.pushViewController(vc, animated: true)
}
func setupSearchController(for vc: HomeViewController) {
let resultVC = ResultViewController(viewModel: SearchViewModel(networkService: networkService)
let searchController = UISearchController(searchResultsController: resultVC) // searchController를 통해 검색 시 resultVC로 바로 전환
searchcontroller.searchResultsUpdator = resultVC // searchController 접근 가능
vc.navigationItem.searchController = searchController
}
}
searchResultsController로 사용되는 VC는 searchController의 searchBar를 클릭하면 바로 해당 VC로 전환된다.
SearchController는 HomeVC에 배치되어있으므로 ResultVC에서는 SearchController에 직접 접근할 수 없다.
그때문에 searchController.searchResultsUpdator = resultVC 코드를 작성하였다.
delegate 패턴과 비슷한데, UISearchResultsUpdating 프로토콜을 채택한 객체는 searchResultsUpdator로 할당할 수 있다.
extension ResultViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
bind(searchController: searchController)
}
}
해당 프로토콜을 채택하면 updateSearchResults 함수를 필수 구현해야하고, 해당 함수에서 searchController에 접근 가능하다.
searchController의 text를 사용하기 위해 updateSearchResults 함수 내에서 바인딩을 진행하였는데, 이때문에 처음과 같은 버그가 발생한 것이었다.
updateSearchResults 함수는 searchBar의 텍스트가 바뀔 때마다 호출되기 때문에, 그때마다 바인딩이 계속해서 되고 있다.
그래서 텍스트를 입력한 수만큼 이벤트가 방출된 것이다.
ResultVC에서 updateSearchResults 함수 없이 searchBar의 text를 접근할 수 있도록 이니셜라이저에 직접 주입하는 방식으로 구현하였다.
class HomeViewController: UIViewController {
...
let searchKeywordRelay = BehaviorRelay<String>(value: "")
private func bind() {
if let searchBar = navigationItem.searchController?.searchBar {
searchBar.rx.text.orEmpty
.throttle(.milliseconds(500), scheduler: MainScheduler.instance)
.bind(to: searchKeywordRelay) // searchBar의 텍스트와 바인딩
.disposed(by: disposeBag)
}
...
}
HomeVC에서 searchKeywordRelay 속성을 선언하여 searchBar의 text와 바인딩 해주었다.
class AppCoordinator {
func setupSearchController(for vc: HomeViewController) {
let resultVC = ResultViewController(
viewModel: SearchViewModel(networkSErvice: networkService),
searchKeyword: vc.searchKeywordRelay.asObservable()
)
let searchController = UISearchController(searchResultController: resultVC)
vc.navigationItem.searchController = searchController
}
}
ResultVC에도 searchKeyword 속성을 선언하여 Coordinator에서 ResultVC 생성 시 HomeVC의 searchKeywordRelay를 주입받아 searchBar에서 바인딩된 text를 사용할 수 있도록 하였다.
final class ResultViewController: UIVIewController {
private let searchKeyword: Observable<String>
override func viewDidLoad() {
super.viewDIdLoad()
bind()
}
}
bind 함수의 호출은 viewDidLoad에서 1번만 일어나므로 이제 중복으로 바인딩 되지 않고 1번만 일어난다!

의도했던대로 동작하는 것을 확인하였다.