안녕하세요 dvHuni입니다!
오늘은 오랜만의 Daily Issue를 가지고 오게 되었는데요-!
사이드 프로젝트를 하면서 발생했던 문제를 가지고 왔습니다 .. 두둥-! 🥁
MVVM 환경에서 StoryBoard로 만들어진 ViewController에 ViewModel 주입하기 입니다!
이게 왜 문제가 됐을까요 하하하하ㅏ하ㅏㅏ 다 제가 초짜이기 때문~
자, 시작해볼까요~!! 🏃♂️
MVVM 환경을 구축을 해야되는 상황에서 ViewModel의 주입에 대한 고민을 하고있었습니다.
일반적으로 ViewController에서 사용할 ViewModel을 생성해서 사용하는것 같더라구요..
class ViewController: UIViewController {
var viewModel = ViewModel()
}
이런 경우에는 화면 이동 시 ViewModel에 초기값을 주고 싶은 경우, 다음과 같이 작성하겠죠?
class ViewController: UIViewController {
func navigateToNextViewController() {
let nextViewController = NextViewController() // NextViewModel은 NextViewController에서 생성됨
let nextViewModel = NextViewModel()
nextViewModel.someProperty = .zero
nextViewController.viewModel = nextViewModel
}
}
하지만 제가 걱정했던 부분은 👆 위의 코드에서 viewModel을 주입하는 코드를 까먹어도 문제없이 동작한다는 점 입니다.
NextViewController
에서는 NextViewModel
을 항상 초기화 하기 때문입니다.
물론 깜빡하는 상황이 자주 일어날 것 같진 않지만, 제 생각에는 충분히 리스크가 있는 코드라고 생각합니다. 🥲
따라서 initializer를 통해 viewModel을 항상 주입 하도록 만드는데요,
class BaseViewController<T: BaseViewModel>: UIViewController {
let viewModel: T
init(_ viewModel: T) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil) // Code로 ViewController를 생성하기 때문에 nibName과 bundle은 필요없습니다
}
required init?(coder: NSCoder) {
fatalError("init(coder: ) has not been implemented")
}
}
class BaseViewModel { }
/// in Use
final class NextViewController: BaseViewController<NextViewModel> {
...
}
final class NextViewModel: BaseViewModel {
...
}
// ViewController.swift
class ViewController: BaseViewController<BaseViewModel> {
...
...
func navigateToNextViewController() {
let viewModel = NextViewModel()
let viewController = NextViewController(viewModel)
present(viewControoler, animated: false, completion: nil)
}
}
navigateToNextViewController
메소드에서 볼 수 있듯이, NextViewController
초기화 시 NextViewModel
을 주입하여 사용합니다.
물론 DI적인 측면에서는 해당 방법도 지양해야할 방법이지만, 차차 수정해 나갈 생각입니다 😅
각설하고, 그렇다면 뭐가 문제냐 !!
코드로 뷰를 짜게되면 init을 하기 때문에 문제가 없지만, 스토리보드로 init이 되는 화면에 대해서는 처리가 안된다는 점 입니다.
StoryBoard에서 기본적으로 생성되는 ViewController의 경우 따로 아무것도 설정하지 않아도,
ViewController로 잘 연결되어있습니다.
그럼 이 ViewController도 BaseViewController
를 상속받아 기본적으로 ViewModel
을 주입받게 만들려면 어떻게 해야할까요 ...?
즉, StoryBoard로 생성되는 ViewController에 init 시점에 ViewModel 주입받기가 오늘의 이슈입니다 🤣
그럼 해결하러 출발~ 🏃♂️
우선 StoryBoard로 ViewController를 생성 할 때는 다음과 코드를 사용하게 됩니다.
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateInitialViewController() as? ViewController
이때 사용하는 instantiateInitialViewController()
!!
느낌만 봤을때는 ViewController으로 캐스팅하니까 정의해두었던
ViewController의 initializer를 이용할 것 같진 않습니다.
자세히 알아보기 위해 documentation을 보면..
using its init(coder:) 메소드를 이용한다고 적혀있군요!!
그렇다면 init(coder:) 를 작성해볼까요 ..?
class BaseViewController<T: BaseViewModel>: UIViewController {
let viewModel: T
init(_ viewModel: T) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil) // Code로 ViewController를 생성하기 때문에 nibName과 bundle은 필요없습니다
}
init?(_ coder: NSCoder, _ viewModel: T) {
self.viewModel = viewModel
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder: ) has not been implemented")
}
}
👆 위 코드에서 failable initializer를 잘 봐주세요
다들 아시겠지만 .. 😃
super.init(coder:)는 UIViewController?를 리턴하기 때문에,
nill을 리턴하게 되면 초기화가 되지않는다는 의미입니다.
즉, 생성자에서 초기화가 되지 않을 수 있기 때문에 failable initializer 표시인 ?가 붙게되는 것 입니다!!
그러면 이걸로 끝난걸까요 ..? 정말로 instantiateInitialViewController()
메소드가 저 initializer를 탈까요 ..?
viewModel도 안넘겨줬는데 ..??
ㅋㅋㅋㅋ 당연히 실패합니다.
그러면.. coder와 viewModel을 넘겨주어 작성한 initializer를 사용하는 방법이 있으면 좋을텐데...
라고 생각한다면
UIViewController의 initializer를 조금 더 살펴봐야죠?
👆 위와같이 여러가지 생성자가 있네요 !!
항상 써왔던 instantiateInitialViewController()
도 보이고..
그 밑에 instantiateInitialViewController(creator:)
가 보이네요 ?
closure에서 coder를 받아서 ViewController를 return합니다.
오!! 저희가 사용하려는 initializer를 사용할 수 있을 것 같습니다.
바로 적용해보죠!
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateInitialViewController { coder -> ViewController in
let viewModel = ViewModel()
viewModel.viewState = .initalizer
return .init(coder, viewModel) ?? ViewController(ViewModel())
}
코드를 조금 살펴봅시다.
coder → ViewController
이부분은 coder를 통해 리턴하길 원하는 UIViewController의 타입을 지정할 수 있습니다.
저희가 ViewController 타입을 리턴하길 원하기 때문에 해당 코드를 작성하게 된 것이구요.
마지막 return .init(coder, viewModel) ?? ViewController(ViewModel())
코드를 보면
작성했던 initializer가 failable이고, 우리가 리턴해야 할 녀석은 optional이 아닌 ViewController이기 때문에
초기화에 실패했을 때의 대한 코드입니다.
실패하진 않을것 같습니다만 ..ㅎ
자 .. 해당 코드는 AppDelegate나, SceneDelegate 등 스토리보드로 만든 화면을
코드로 호출 할때 사용합니다.
화면이동을 예를 들면 다음과 같겠죠.
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateInitialViewController { coder -> ViewController in
let viewModel = ViewModel()
viewModel.viewState = .initalizer
return .init(coder, viewModel) ?? ViewController(ViewModel())
}
present(viewController, animated: true, completion: nil)
후후.. 코드가 무쟈게 더럽지만... 드디어 해결... !! 나에겐 시간이없다..
+++
근데 문제가 있습니다.
instantiateInitialViewController(creator:)
요 메소드가 iOS13 부터 지원합니다. 😱
그럼 이전 버전에 대해서는 이번 방법을 사용하지 못할 것 같네여 .. 😭
이전 버전에 대한 해결책이 있는지는.. 조금 더 알아보겠습니다.... (댓글로 알려주시면 더욱더 감사)
오랜만에 적어본 Daily Issue인데요 🥲
앞으로는 문제 발생 및 해결 뿐만이 아니라, 자주 까먹어서 구글 선생님을 찾게되는
내용들도 함께 포스팅 해보려고 합니다.
오늘도 읽어주셔서 감사합니다 🙇🏻♂️