룰루랄라 신나게 코딩하고 있었다. 화면전환만 연결해주면 되는데
근데! Storyboard로 연결한 객체 UILable, UIImageView 등 모든 IBOutlet 객체가 nil 인 것이다.
맨붕이 왔다. 왜지? Storyboard에서 UIViewConroller로 전환하는게 실패한 것인가?
아니다. 캐스팅까지 완벽하게 되었는데!
정답은 UIViewController에 대한 내 이해도가 부족했기 때문에 일어난 문제였다.
히히 내가 어떤 점의 이해가 부족했는지 살펴보자
class FirstViewController: UIViewController {
private let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "SecondViewController") as? SecondViewController
required init?(coder: NSCoder) {
super.init(coder: coder)
print("First VC init] textLabel is nil? \(secondVC?.textLabel == nil)")
}
@IBAction func tappedButton(_ sender: Any) {
guard let secondVC else { return }
secondVC.textLabel.text = "이제 될 꺼임"
print("tappedButton] textLabel is nil? \(secondVC.textLabel == nil)")
navigationController?.pushViewController(secondVC, animated: true)
}
}
class SecondViewController: UIViewController {
@IBOutlet weak var textLabel: UILabel!
var text: String?
required init?(coder: NSCoder) {
super.init(coder: coder)
print("Second VC init] textLabel is nil? \(textLabel == nil)")
}
override func viewDidLoad() {
super.viewDidLoad()
textLabel.text = text
}
}
아주 가볍게 그 때의 상황을 재현해 보았다.
Storyboard에서 가운데 ViewController가 FirstViewController
그 오른쪽이 SecondViewController
이다.
버튼을 누르면 SecondViewContoller로 전환해주는 간단한 앱을 만들었다.
위 코드는 실행 될까? 안될까?
버튼을 누르는 순간! 삐빅 에러를 뱉는다.
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
nil 인데 암시적 unwrapping해서 에러라고 함
ViewController가 정상적으로 객체로 만들어졌는데 왜? 안될까?
정답은 ViewController의 생명주기를 무시했기 때문이다.
간단한 생명주기로 loadView
-> viewDidload
-> viewWillAppear
... 이 있다.
괜히 이러한 생명주기가 있는게 아니다.
내가 무시한건 저 load
키워드와 관련된 생명주기다.
load : 적재하다.
객체가 만들어지면 안에 있는 IBOutlet 만들어 질까? -> X
View 계층에 올려지면 IBOutlet 만들어 진다. -> O
그럼 다시 에러를 보자.
난 navigation에 Push되기 전에 Label에 접근했다.
즉, 컴포넌트들이 load 되지 않은 상태에 접근했기에 nil 인 값이었던 것이고 에러가 난 것이다.
그럼 어떻게 전달해야 할까?
import UIKit
class FirstViewController: UIViewController {
private let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "SecondViewController") as? SecondViewController
required init?(coder: NSCoder) {
super.init(coder: coder)
print("First VC init] textLabel is nil? \(secondVC?.textLabel == nil)")
}
@IBAction func tappedButton(_ sender: Any) {
guard let secondVC else { return }
secondVC.text = "이제 될 꺼임"
print("tappedButton] textLabel is nil? \(secondVC.textLabel == nil)")
navigationController?.pushViewController(secondVC, animated: true)
}
}
class SecondViewController: UIViewController {
@IBOutlet weak var textLabel: UILabel!
var text: String?
required init?(coder: NSCoder) {
super.init(coder: coder)
print("Second VC init] textLabel is nil? \(textLabel == nil)")
}
override func loadView() {
print("SecondVC loadView-super.loadView() before] textLabel is nil? \(textLabel == nil)")
super.loadView()
print("SecondVC loadView-super.loadView() after] textLabel is nil? \(textLabel == nil)")
}
override func viewDidLoad() {
super.viewDidLoad()
textLabel.text = text
}
}
오 이제 잘 된다. 왜냐하면 load된 이후에 Label에 텍스트를 지정해 주었기 때문이다.
UIViewConroller를 간단한 Class로 생각하지 마라!
화면을 구성하는 복잡한 아이이다. 괜히 면접 단골로 View Life Cycle을 물어보는게 아니다.
UIViewController에 문제가 생겼다면 혹시 내가 Life Cycle을 무시하는게 아닌지 생각해보자!
그럼 출력 값을 볼까?
super.loadView()
과정 이후에 textLabel 객체가 생성됨을 알 수 있다.
즉! UIViewController가 loadView를 마쳐야 IBOutlet의 객체들이 생성되는 것이다!
+ 추가로 이렇게 구성해도 됨
import UIKit
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func tappedButton(_ sender: Any) {
if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "SecondViewController") as? SecondViewController {
vc.completeHandler = {
vc.textLabel.text = "이제 될꺼임"
}
navigationController?.pushViewController(vc, animated: true)
}
}
}
class SecondViewController: UIViewController {
@IBOutlet weak var textLabel: UILabel!
var completeHandler: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
completeHandler?()
}
}