UIViewController 서브클래스의 custom initializer 만들기('required' initializer 'init(coder:)' must be provided by subclass of 'UIViewController')

jane·2022년 6월 5일
2

iOS

목록 보기
22/32

UIViewController을 상속받아서 커스텀뷰컨을 만드는 작업은 정말.. 항상하는 작업이다.
대부분은 따로 직접 이니셜라이저를 구현하지 않고 사용하기 때문에 특별한 문제가 없다.

하지만... 뷰컨에 뷰모델을 생성자 주입하려고 이니셜라이저를 만들었더니...
이런 에러가 나왔다.
UIViewUIViewController를 상속해서 이니셜라이저를 작성하려고 하면 'required' 이니셜라이저인 init(coder:)을 작성하라고 한다.

항상 궁금했던 부분들

  1. 왜 대체 required init을 구현하라는 것일까
  2. 그럼 원래 이니셜라이저가 없었을때는 왜 구현하라고 안했나
  3. 뷰컨을 nib/storyboard/코드로 구현했을때 각각 어떻게 작성해야할까

required init 구현해야하는 이유


UIViewControllerNSCoding을 채택한다.
위의 NSCoding의 정의부분을 보면... 지정생성자가 init?(coder: NSCoder)이다.
NSCoding 프로토콜을 구현하는 클래스들은 모두 저 init?(coder: NSCoder)구현해야 하고, 그 클래스들을 상속받는 자식 클래스들도 마찬가지로 동일하게 구현해야한다.

근데 왜 required 키워드가 붙는거지?

프로토콜에 정의되어있는 이니셜라이저를 구현하면 required 키워드가 붙는다고 한다.

그럼 원래 이니셜라이저가 없었을때는 왜 구현하라고 안했나

자식 클래스에서 이니셜라이저를 따로 구현하지 않는 경우 부모의 이니셜라이저를 자동으로 상속받기 때문에 그렇다.
그래서 보통의 경우에는 저 UIViewController에 구현되어있는 init?(coder: NSCoder)가 자동으로 상속된 것...
하지만 자식 클래스에서 이니셜라이저를 구현하는 이 상황의 경우에는 자동 상속이 안되어서 구현하라는 에러가 뜬 것이다 ㅎㅎ

custom initializer 구현 방법 세가지

코드

init(viewModel: ProjectListViewModel) {
    self.viewModel = viewModel
    super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    //super.init(coder: coder) 이것도 됨
}

코드로 구현했으니 init(coder:)은 실행되지 않고 위에 구현해준 이니셜라이저가 실행이 될 것이다.
그래서 어짜피 실행이 되지 않을 테니 fatalError 을 둬도 괜찮다.
그리고 nib을 사용하지 않으니 init(nibName:, bundle:)에는 각각 nil을 전달해준다.

nib

init(viewModel: MovieListCollectionViewModel, nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    self.viewModel = viewModel
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

nib로 구현했으니 UIViewController
의 이니셜라이저인 init(nibName:, bundle:)을 이용한다.

storyboard

지금까지 설명한 코드, nib으로 구현하는 경우는 init(coder:)가 안불리게 redirect가 가능하기 때문에 비교적 간단했다.

그러나 문제는 바로 스토리보드로 구현하는 경우인데,
스토리보드로 뷰를 생성할 때는 진짜 무적권 init(coder:)가 불리는데
이때 뭔가 추가적인 설정을 해줄수가 없음 ....
스택오버플로우 이 사람도 스토리보드에서는 하지 말라고 그런다... 방법이 없다고 ㅋㅋㅋ

storyboard.instantiateViewController(withIdentifier: "ViewController")

이 메서드를 이용해서 뷰컨트롤러를 초기화했을 때 required initializer인 init(coder:)가 진짜 무조건 ㅋ 불림

그런데 또 super.init?(coder aDecoder: NSCoder)을 다른 파라미터와 함께 초기화하는 일은 불가능하다.... ㅎ ㅏ

더 구글링해보니깐 여러 사람들이 아주 눈물나는 노력을 했음...
이런식으로 팩토리 메서드를 만들어서 instantiate하고 나서 바로 프로퍼티로 주입해주는 방식이나

더 이상한 UIApplication에 접근해서 ㅎ.. 어쩌고 저쩌고

헬파티라고 생각한 순간 이 글을 만났다.

바로 iOS 13부터 새로운 메서드가 나왔는 것이다.

storyboard.instantiateViewController(identifier:, creator:)

바로 이것 ㅎ
설명을 보면 커스텀 이니셜라이저 코드를 사용하여 스토리보드에서 뷰컨트롤러를 생성할 때 사용한다고 나와있다.

이전까지 우리가 자주 사용하던 아래 메서드로는 커스텀 이니셜라이저를 사용해서는 뷰컨을 생성하지 못한다. 여기 설명에는 custom이 없는것좀 봐라

그니까 이제는 스토리보드로 뷰컨트롤러를 이니셜라이즈하는 방식이 두가지가 있는 것이다. 그냥과 커스텀

storyboard.instantiateViewController(identifier:, creator:)

의 공식 문서에 나와있는대로 커스텀 이니셜라이져에도 coder: NSCoder을 파라미터로 전달해주고, 상속받은 init(coder:)도 불러줬다

    init?(viewModel: MovieListViewModel, coder: NSCoder) {
        self.viewModel = viewModel
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

기존 방식: 에러

이렇게 실행하면 required init?(coder:) 타는 기존의 방식 대신에

    window?.rootViewController = storyboard.instantiateViewController(withIdentifier: "MovieListViewController")

새로운 방식: 성공

creator 파라미터가 추가된 이 새로운 방식을 사용하면 정상적으로 뷰컨이 생성된다~!!!!

    window?.rootViewController = storyboard.instantiateViewController(identifier: "MovieListViewController", creator: { creater in
                let viewModel = MovieListViewModel(defaultMoviesUseCase: DefaultMoviesUseCase(moviesRepository: DefaultMoviesRepository(apiManager: APIManager())))
                let viewController = MovieListViewController(viewModel: viewModel, coder: creater)
                return viewController
            })

Reference

https://www.swiftbysundell.com/tips/handling-view-controllers-that-have-custom-initializers/

profile
제가 나중에 다시 보려고 기록합니다 ✏️

0개의 댓글