
회사 프로젝트를 진행하던 중, 커스텀으로 만든 네비게이션 바의 뒤로가기 버튼의 이벤트를 구현하려 하였다. Coordinator 패턴을 적용하였기 때문에 Coordinator에서 ViewController에 있는 네비게이션 바에 접근하고, 버튼 이벤트를 Combine을 통해 버튼의 tapPublisher를 구독하려했다.
//예시
class Coordinator {
func start() {
let viewModel = ViewModel()
let vc = ViewController(viewModel: viewModel)
//Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
vc.navigationBar.leftButton.tapPublisher.sink { [weak self] in
guard let self else { return }
self.navigaitonController.popViewController(animated: true)
}
}
}
(에러메세지 : Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value)
하지만 위와 같이 nil 값에 접근했다는 오류가 발생하였다. ViewController에서 커스텀으로 만든 navigationBar가 nil이였다는 것이다. 원인은 무엇이고 해결방법은 무엇일까?
프로젝트에서 ViewController와 View들은 모두 xib파일로 관리하고 있다. 따라서, 각 xib 파일과 swift파일에서 @IBOutlet을 통해서 연결되어 있다.
(예시)
화면과 같이, UIKit에서 xib파일 또는 Storyboard 파일로 UI 작업을 하게 되면 필연적으로 @IBOutlet을 만나게 된다. @IBOutlet으로 선언된 View 변수는 최초에 암묵적 언래핑으로 선언되어 있는데, 그 이유로 @IBOutlet 변수는 ViewController의 init과 동시에 초기화되지 않기 때문이다. 이것은 xib파일로 만든 ViewController에 모두 해당된다.
xib 파일로 ViewController를 만들게 될 경우, init(nibName:bundle:) 메소드를 통해 초기화 되는데, 이때 nibName을 통해 nib파일을 찾게 된다(xib파일은 컴파일 시 nib파일로 아카이빙됨).
이후, init?(coder:)를 통해 nib 파일을 언아카이빙 하게 되고 그 다음에 awakeFromNib() 메소드를 통해 @IBOutlet, @IBAction과 xib 파일의 View들이 바인딩 되어 nil이 아님이 보장되는 것이다.
(자세한 초기화 과정은 아래의 참고링크를 확인하길 바람)
그렇기 때문에 내가 ViewController를 초기화 시키고 네비게이션 바에 접근했던 시기는 아직 @IBOutlet과 xib파일의 View가 바인딩이 되지 않아 nil값이 할당되는 시기였던 것이다.
ViewController를 초기화 시킨 이후, loadViewIfNeeded() 메소드를 호출한 다음, 네비게이션 바에 접근하였다.
class Coordinator {
func start() {
let viewModel = ViewModel()
let vc = ViewController(viewModel: viewModel)
vc.loadViewIfNeeded()
//에러 없이 View에 접근 성공!
vc.navigationBar.leftButton.tapPublisher.sink { [weak self] in
guard let self else { return }
self.navigaitonController.popViewController(animated: true)
}
}
}
공식문서에 따르면, loadViewIfNeeded() 메소드는 ViewController의 view가 아직 로드되지 않았을 때 로드해주는 메소드라 설명한다.
코드상으로 나는 ViewController를 초기화한 이후 view에 접근해야 했는데 view가 로드된 상태가 아니였기 때문에, 해당 메소드를 통해 view를 로드하여 접근할 수 있었다.
(도움을 얻은 글 : https://jryoun1.github.io/ios/ViewControllerLifeCycle/#initcoder)
(공식문서 awakeFromNib() : https://developer.apple.com/documentation/objectivec/nsobject/1402907-awakefromnib)
(공식문서 init(nibName:bundle:) : https://developer.apple.com/documentation/uikit/uiviewcontroller/1621359-init)
(공식문서 loadViewIfNeeded(): https://developer.apple.com/documentation/uikit/uiviewcontroller/1621446-loadviewifneeded)