안녕하세요
iOS 개발을 하며 겪게되는 문제들을 정리하기 위해 앞으로 트러블 슈팅과 관련된 글을 적어보려고 합니다.
목차는 다음과 같습니다.
- 발생한 문제
- 해결
- 회고
발생한 문제
저는 지금 MVVM디자인 패턴을 사용하여 개발을 진행하고 있습니다.
ViewModel과 ViewController를 바인딩 시켜주기 위한 시점으로 ViewDidLoad가 호출되는 시점을 그 시점으로 하자고 판단하였습니다.
현재 프로젝트에서는 RxSwift를 사용하여 옵저버 패턴을 구현하고 있습니다.
public extension Reactive where Base: UIViewController {
var viewDidLoad: ControlEvent<Void> {
let source = self.methodInvoked(#selector(Base.viewDidLoad)).map { _ in }
return ControlEvent(events: source)
}
}
특정함수가 호출되는 시점을 이벤트화 하기위해 Reactive타입을 확장하였습니다.
아래는 ViewController의 생성자 입니다.
init() {
super.init(...)
// viewDidLoad이벤트를 바인딩
}
제가 평소에 생각하기론 viewDidLoad는 뷰가 로드된 상태 즉,
뷰가 화면에 표시될 때 최초 한번만 호출된다 정도로만 인지하고 있었습니다.
따라서 생성자에서 ViewModel의 input으로 viewDidLoad가 호출되는 이벤트를 바인딩 해주면 되겠다고 판단하였습니다.
그런데, 생성자보다 viewDidLoad가 먼저호출되버려, 이벤트를 구독하기전 이벤트가 종료되버리는 상황이 발생했습니다.
어떻게 이런일이..? 😵💫
뷰컨트롤러의 생명주기에 대한 학습이 부족하다고 판단하여, 공식문서를 다시 읽어보았습니다.
해결
ViewController의 view프로퍼티는 옵셔널 타입으로 생성시 바로 UIView가 할당되지 않습니다.
해당시점은 view프로퍼티가 호출되는 시점입니다.
view에 접근하면 loadView를 호출하여 view를 생성합니다.
loadView가 view를 생성하면, 호출되는 매서드가 viewDidLoad입니다.
즉, 해당 매서드를 오버라이딩한 함수는, view의 속성을 변경하기에 적합한 공간입니다.
override func viewDidLoad() {
super.viewDidLoad()
// view를 수정하기에 적합한 공간
}
그럼 왜 init함수 블록이 완료되기 전에 viewDidLoad가 호출된 것인지 알 수 있습니다.
바로 블록이 끝나기전에 view에 접근을 하였고
loadView -> viewDidLoad가 동기적으로 호출된 것입니다. (해당 메서드들은 모두 메인쓰레드에서 실행됨)
init() {
print("[init] init호출")
super.init(nibName: nil, bundle: nil)
// view에 접근
view.backgroundColor = .white
print("[init] super.init호출 이후")
}
각각의 시점마다 로그를 찍어보면 다음과 같은 결과가 나오게됩니다.
[init] init호출
[view프로퍼티] view가 호출되었음
[loadView()] view를 생성할거임
[loadView()] view가 생성되었음
[view프로퍼티] view가 호출되었음
[view프로퍼티] view가 호출되었음
[viewDidLoad()] viewDidLoad, view가 생성되었음
[init] super.init호출 이후
전체코드입니다.
class ViewController3: UIViewController {
override var view: UIView! {
get {
print("[view프로퍼티] view가 호출되었음")
return super.view
}
set {
super.view = newValue
}
}
init() {
print("[init] init호출")
super.init(nibName: nil, bundle: nil)
// view에 접근
view.backgroundColor = .white
print("[init] super.init호출 이후")
}
required init?(coder: NSCoder) { fatalError() }
override func loadView() {
print("[\(#function)] view를 생성할거임")
super.loadView()
print("[\(#function)] view가 생성되었음")
}
override func viewDidLoad() {
super.viewDidLoad()
print("[\(#function)] viewDidLoad, view가 생성되었음")
}
}
회고
viwDidLoad는 뷰컨트롤러의 view프로퍼티가 생성될 때 한번만 호출됨으로, 사실상 ViewController인스턴스당 한번만 호출됩니다.
따라서 뭔가 최초로 한번만 실행되는 작업(ViewModel바인딩)과 같은 작업을 실행하기 적합합니다.
따라서 해당 함수가 호출되는 시점을 명확하개 파악하는 것이 중요하며,
위와 같은 경우 init함수 블록내에서 view프로퍼티에 접근하는 작업이 없어야 정상적으로 동작하게 됩니다.