cleanUrl: /programming/do-not-blame-mvc-3/
**“버튼이 눌렸을 때 다음 화면을 보여준다”**는 기능을 구현해봅시다. 이렇게 하면 되겠죠?
@IBAction func buttonClicked(_ sender: Any?) {
let nextViewController = NextViewController()
nextViewController.importantInfo = self.importantInfo
nextViewController.anotherInfo = self.anotherInfo
navigationController?.push(nextViewController)
}
복잡 할 것이 없는 코드입니다. 여기까진 말이죠. 하지만 현실에서, 이런 종류의 접근방식은 ViewController의 크기를 순식간에 늘려 버립니다.
만약 버튼을 눌렀을 때, 로그인이 안 되어있다면 NextViewController
가 LoginViewController
를 보여줘야 한다면 어떨까요?
class MyViewController: UIViewController {
@IBAction func buttonClicked(_ sender: Any?) {
if user.isLoggedIn {
let nextViewController = NextViewController()
nextViewController.importantInfo = self.importantInfo
nextViewController.anotherInfo = self.anotherInfo
navigationController?.push(nextViewController)
}
else {
let loginVC = LoginViewController()
loginVC.someContext = someContext
navigationControler?.push(loginVC)
}
}
}
여기에 로직이 더 추가되면 어떻게 될까요? 앞선 ViewController에 따라 NextViewController가 달라져야 한다면? 혹은 시간대나 위치에 따라 달라져야한다면? 등등…
로직이 추가되면 될 수록, 이 네비게이션 코드는 아주 많이 길어 질 수 있습니다. 아마 이 글을 읽고 있는 많은 분들의 ViewController에도 비슷하게 짧지 않은 네비게이션 관련 코드들이 있을테지요.
하지만 ViewController가 아니라면, 누가 Navigation코드를 담당 할 수 있을까요?
이에 대해 Coordinator패턴등 의 디자인패턴도 좋은 대안이 되겠지만, 제게는 더 Obvious한 대안이 떠오릅니다. 바로 UINavigationController
지요!
위의 코드를 리팩토링해 보겠습니다.
class MyNavigationController: UINavigationController {
func showLoginViewController(with context:Context) {
let loginVC = LoginViewController.storyboardInstance
loginVC.context = context
push(loginVC)
}
func showNextViewController(with foo:Foo, bar: Bar) {
let nextVC = NextViewController.storyboardInstance
nextVC.foo = foo
nextVC.bar = bar
push(nextVC)
}
}
class MyViewController: UIViewController {
var myNavVC: MyNavigationController {
return navigationController as! MyNavigationController
}
@IBAction func buttonClicked(_ sender: Any?) {
if user.isLoggedIn {
myNavVC.showLoginViewController(with: context)
}
else {
myNavVC.showNextViewController(with: foo, bar: bar)
}
}
}
@IBAction
의 코드가 확연히 줄어든 것을 볼 수 있습니다. 그래도 전체적인 코드의 양 자체는 비슷한 거 아니냐구요? 그렇게 생각할 수도 있습니다. 하지만 이렇게 리팩토링한 결과, 우리는 여러가지 효과를 누릴 수 있습니다.
NextViewController()
와 같은 방법으로 NextViewController를 초기화 했지만, 사실 xib로 초기화 할 수도 있는 거고, 경우에 따라 navigationStack에 이미 있었던 인스턴스를 재활용 할 수도 있죠. 그 모든 책임이 이제 UINavigationController에게 넘어갑니다.특히, Push Notification의 userInfo를 바탕으로 네비게이션을 하는 경우에, 이런 접근은 진가를 발휘하게 됩니다. 먼저 저희 팀에서 기존에 Push Noti정보로 네비게이션을 하는 코드는 다음과 같았습니다.
class AppDelegate: UIApplicationDelegate {
func application(_ app: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler _: @escaping (UIBackgroundFetchResult) -> Void) {
let pageNavigator = PageNavigator.shared
pageNavigator.targetPageInfo = userInfo
rootNavigationController.viewControllers = [rootNavigationController.viewControllers[0]]
}
}
class FirstViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated:animated)
if PageNavigator.shared.targetPageInfo["destination"] != FirstViewController.className {
let nextVC = SecondVC.storyboardInstance
navigationController?.push(nextVC)
}
}
}
class SecondViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated:animated)
if PageNavigator.shared.targetPageInfo["destination"] != SecondViewController.className {
let nextVC = ThirdViewController.storyboardInstance
navigationController?.push(nextVC)
}
}
}