StoryBoard에 ViewModel 주입하기

sanghoon Ahn·2021년 10월 31일
2

Daily Issue

목록 보기
7/9
post-thumbnail

Daily Issue #7

안녕하세요 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인데요 🥲

앞으로는 문제 발생 및 해결 뿐만이 아니라, 자주 까먹어서 구글 선생님을 찾게되는

내용들도 함께 포스팅 해보려고 합니다.


지적이나 질문은 저에게 큰 도움이 됩니다 🤓

오늘도 읽어주셔서 감사합니다 🙇🏻‍♂️

profile
hello, iOS

0개의 댓글