우리가 AutoLayout을 하나 연결하게 되면, iOS는 어떤 과정으로 화면을 그리게 되는지에 대해서 알아보고자 합니다.
먼저 전반적인 흐름을 설명드리겠습니다/
우리가 화면을 터치하거나, 네트워킹요청이나 키보드가 나타나거나 사라지는 애니메이션 그리고 타이머와 같은 트리거의 역할을 하는 것들을 이벤트라고 칭합니다.
아래 예시를 보면서 설명드릴게요.
각 하위 뷰들의 Bounds 값을 정해줍니다. Bounds는 View 자기 자신을 기준으로 결정하는 좌표계입니다. (frame은 superView를 기준으로 잡은 좌표계입니다.)
마치, Bounds를 잡는 것은 View 각각의 차지할 크기나 각자가 가지고 있어야할 범위를 가지고 있는 개념으로 이해됩니다. 즉, 아직은 View 간의 관계는 정의되지 않은거죠. 그냥 각자 자기 자신의 값을 소유하고 있는 겁니다.
setNeedDislay
를 통해 다음 드로잉 사이클 때, 해당 layout을 그리도록 메시지를 전달하고 있습니다.(위에서부터 아래로 랜더링함)
그러면, 어떤식으로 호출되는지 print 메소드의 도움을 받아 확인해보겠습니다.
코드는 아래와 같습니다.
import UIKit
class ViewController: UIViewController {
// 메모리에 로드할 때, 호출
override func loadView() {
super.loadView()
print("ViewController: loadView 호출")
}
// 메모리에 로드된 이후에 호출
override func viewDidLoad() {
super.viewDidLoad()
print("ViewController: viewDidLoad 호출")
}
// display 되기 직전에 호출
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("ViewController: viewWillAppear 호출")
}
// 제약조건을 업데이트 할 때, 호출
override func updateViewConstraints() {
super.updateViewConstraints()
print("ViewController: updateViewConstraints 호출")
}
// 하위 뷰의 레이아웃을 계산할 때, 호출
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("ViewController: viewWillLayoutSubviews 호출")
}
// 하위 뷰의 레이아웃을 계산을 마친 후, 호출
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("ViewController: viewDidLayoutSubViews 호출")
}
}
import UIKit
class SuperView: UIView {
override func updateConstraints() {
super.updateConstraints()
print("SuperView: UpdateConstraints 호출")
}
override func setNeedsLayout() {
super.setNeedsLayout()
print("SuperView: setNeedsLayout 호출")
}
override func layoutSubviews() {
super.layoutSubviews()
print("SuperView: layoutSubviews 호출")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
print("SuperView: draw 호출")
}
}
import UIKit
class SubView: UIView {
override func updateConstraints() {
super.updateConstraints()
print("SubView: UpdateConstraints 호출")
}
override func setNeedsLayout() {
super.setNeedsLayout()
print("SubView: setNeedsLayout 호출")
}
override func layoutSubviews() {
super.layoutSubviews()
print("SubView: layoutSubviews 호출")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
print("SubView: draw 호출")
}
}
특별한 로직은 전혀 없고, 단순히, 어떤 메소드가 어떤 타이밍에 호출되는지 print 문만 작성한 코드입니다.
실행결과는 아래와 같습니다.
# 메모리에 저장
ViewController: loadView 호출
ViewController: viewDidLoad 호출
ViewController: viewWillAppear 호출
# Constraint 계산 (SubView -> SuperView -> ViewController 순으로 실행)
SubView: UpdateConstraints 호출
SuperView: UpdateConstraints 호출
ViewController: updateViewConstraints 호출
# Layout 구성 (ViewController -> SuperView -> SubView 순으로 실행)
ViewController: viewWillLayoutSubviews 호출
SuperView: setNeedsLayout 호출
SuperView: setNeedsLayout 호출
SuperView: setNeedsLayout 호출
SuperView: setNeedsLayout 호출
ViewController: viewDidLayoutSubViews 호출
SuperView: layoutSubviews 호출
SubView: layoutSubviews 호출
SubView: layoutSubviews 호출
# 화면에 그리기 (SuperView -> SubView 순으로 실행)
SuperView: draw 호출
SubView: draw 호출
조금 이 말을 쉽게 설명하자면, 아래처럼 되지 않을까 생각합니다.
- 일단, 메모리에 올려둬봐.
- AutoLayout으로 그려져있네, 그러면 방정식을 풀 듯이 각각의 방정식(x, y, width 그리고 height에 대한 방정식)을 풀어야겠네, 한 번 식 세워봐 어떤 객체랑 어떤 객체가 서로 관계식을 가지고 있는지.
- 이제 방정식 모두 세웠으니까, 식에 맞게 배치를 해야지 배치하자.
- 자 배치 다했다, 이제 그리기만하면된다. 그리자!
방정식이라는 말이 조금 어색하실 수도 있지만, WWDC 2018에서 다음과 같은 그림과 함께 설명하고 있는 내용입니다.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = UIWindow(windowScene: windowScene)
let rootViewController = ViewController()
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
}
}
여기서 보면, window를 생성해서 할당하고 있죠. 그안에 들어가있는 것은 UIViewController 입니다. 이 구조를 생각하시면 좋을 것 같아요.
참고로 방정식을 연산하는 Engine의 과정입니다.
그겋게 layoutSubView가 동작하게되면, View가 엔진에게 연산한 값에 대해서 값을 확인합니다.
하위 뷰에게 Bounds와 Center 값을 설정하게되죠.
하위 뷰에 Bounds와 Center 값을 설정한다는 의미는, bounds는 말그대로 자기 자신을 기준으로 위치와 크기를 결정하는 CGRect 값입니다.(사각형 모양임) 그리고 Center는 뷰 프레임 내에서 사각형의 중심점을 결정하는 겁니다.
위 과정이 layout을 잡는 과정이죠. 크기는 어느정도이다. 그러니 이곳에 레이아웃을 형성하자. 이 과정인거죠.
Bound는 크기를 가지고 있고, 자신을 기준으로 위치를 잡으므로 CGRect값이고, center는 뷰 내부에서의 위치이므로 CGPoint일 수 밖에 없겠죠.
그림으로 보면 다음과 같습니다.
각 단계별 호출하는 메소드 혹은 호출되는 메소드들에 대한 자료입니다.(WWDC18)
코드를 보면서 해당 동작들이 어떻게 동작하는지와 동시에, 안좋은 케이스 그리고 개선하는 것들을 보겠습니다.
override func updateConstraint() {
NSLayoutConstraint.deactivate(myConstraints) // <-- 비활성화
myConstraint.removeAll()
let views = ["text1": text1, "text2": text2]
myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
options: [.aligAllFirestBaseline],
metrics: nil,
views: views)
myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
options: [],
metrics: nil,
views: views)
NSLayoutConstraint.activate(myConstraints) // <-- 활성화
super.updateConstraints()
}
override func layoutSubViews() {
text1.removeFromSuperView() // <-- 상위뷰에서 제거
text1 = nil
text1 = UILabel(frame: CGRect(x: 20, y: 20, width: 300, height: 30))
self.addSubView(text1) // <-- 상위뷰에 추가
text2.removeFromSuperView() // <-- 상위뷰에서 제거
text2 = nil
text2 = UILabel(frame: CGRect(x: 340, y: 20, width: 300, height: 30))
self.addSubView(text2) // <-- 상위뷰에 추가
}
override func updateConstraint() {
if self.myConstraints == nil {
NSLayoutConstraint.deactivate(myConstraints) // <-- 비활성화
myConstraint.removeAll()
let views = ["text1": text1, "text2": text2]
myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
options: [.aligAllFirestBaseline],
metrics: nil,
views: views)
myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
options: [],
metrics: nil,
views: views)
NSLayoutConstraint.activate(myConstraints) // <-- 활성화
}
super.updateConstraints()
}
A객체.bottom = B객체.top + 20
이라는 방정식이 있다면, Constraint는 하위 뷰부터 시작하는데 A객체와 B객체를 모두 가진 SuperView를 연산하기 전까지 해당 식이 풀리지 않을 겁니다. 이렇게 다른 슈퍼뷰 안에 있는 서브뷰와 방정식을 세우면, 아주 조금차이일지 모르겠지만, 연산에 시간이 더 소비될 것 같습니다. 가능하면, 자신의 슈퍼뷰 내에서 해결하려는 습관이 조금이나마 성능 개선에 도움이 될 것 같네요.읽어주셔서 감사합니다.