아래의 UIView 확장을 통해 UIButton의 크기와 위치를 viewDidLoad()에서 frame 기반으로 설정하였을 때, UIButton이 잘못 그려지는 문제가 있었다.
import Foundation
import UIKit
// MARK: - Extension UIView
extension UIView {
var width: CGFloat { return frame.size.width }
var height: CGFloat { return frame.size.height }
var left: CGFloat { return frame.origin.x }
var right: CGFloat { return left + width }
var top: CGFloat { return frame.origin.y }
var bottom: CGFloat { return top + height }
}
// MARK: - ViewController
final class WelcomeViewController: UIViewController {
private let signInButton: UIButton = { ...(생략)... }()
override func viewDidLoad() {
view.addSubview(signInButton)
signInButton.frame = CGRect(x: 20,
y: view.height-50-view.safeAreaInsets.bottom,
width: view.width-40,
height: 50)
}
}
버튼이 위와 같이 화면의 가장 아래에 밀착되어 노출되는 이유는 view.safeAreaInsets.bottom
의 값을 가져오는 방식 때문이다.
view.safeAreaInsets.bottom
값은 뷰컨트롤러의 viewDidLoad()
시점에서는 0으로 설정된다.
view.safeAreaInsets
는 레이아웃이 완전히 설정된 후에 정확한 값을 갖게 된다.
따라서 viewDidLoad()
에서 UIButton의 frame을 설정할 때, view.safeAreaInsets.bottom
이 0으로 처리되면서 버튼의 위치가 아래로 밀착된다.
다이어그램을 통해 safeAreaInsets 값이 viewDidLoad()
시점에서는 0인 이유를 알 수 있다.
init()
으로 뷰컨트롤러가 생성됨
viewDidLoad()
에 의해 뷰가 로드됨
view.frame
)는 설정되지만, 레이아웃과 안전 영역(safe area)은 아직 반영되지 않는다.safeAreaInsets
값은 기본적으로 0
viewWillLayoutSubviews() → viewDidLayoutSubviews()
과정을 통해 뷰의 크기가 정해지고 레이아웃이 적용됨
safeAreaInsets
값이 제대로 반영된다.즉, viewDidLoad()
에서는 safeAreaInsets.bottom
이 아직 0이므로,
y: view.height - 50 - view.safeAreaInsets.bottom
이 부분이 결국 view.height - 50
과 동일하게 계산되어 버튼이 화면 맨 아래로 밀려버리는 것이다.
조금 더 자세하게 설명하자면 아래와 같다.
viewDidLoad()
는 말 그대로, 뷰가 메모리에 로드된 후 실행된다.safeAreaInsets
값은 뷰가 화면에 추가되고 레이아웃이 완료된 후(viewDidLayoutSubviews()
) 정확한 값이 반영된다.viewWillLayoutSubviews()
→ layoutSubviews()
→ viewDidLayoutSubviews()
순서로 호출된다.viewDidLayoutSubviews()
에서 safeAreaInsets.bottom
을 올바르게 가져올 수 있다.// MARK: - ViewController
final class WelcomeViewController: UIViewController {
private let signInButton: UIButton = { ...(생략)... }()
override func viewDidLoad() {
view.addSubview(signInButton)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
signInButton.frame = CGRect(x: 20,
y: view.height-50-view.safeAreaInsets.bottom,
width: view.width-40,
height: 50)
}
}
Safe Area 영역을 가리키는 임의의 UIView (borderView)를 통해 viewDidLoad()
와 viewDidLayoutSubviews()
에서의 뷰컨트롤러의 view 속성의 위치 및 크기를 확인하면 아래와 같다.
override func viewDidLoad() {
super.viewDidLoad()
print("********** viewDidLoad() **********")
view.backgroundColor = .systemGreen
view.addSubview(signInButton)
view.addSubview(borderView)
NSLayoutConstraint.activate([
// ...(borderView 제약조건 생략)...
])
/// `view`
print("width: \(view.width)")
print("height: \(view.height)")
print("left: \(view.left)")
print("right: \(view.right)")
print("top: \(view.top)")
print("bottom: \(view.bottom) \n")
/// `view.safeAreaInsets`
print("top: \(view.safeAreaInsets.top)")
print("bottom: \(view.safeAreaInsets.bottom)")
print("left: \(view.safeAreaInsets.left)")
print("right: \(view.safeAreaInsets.right)")
}
viewDidLoad()에서는 view의 safeAreaInsets 값이 모두 0임을 확인
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("********** viewDidLayoutSubviews() **********")
/// `view.safeAreaInsets`
print("top: \(view.safeAreaInsets.top)")
print("bottom: \(view.safeAreaInsets.bottom)")
print("left: \(view.safeAreaInsets.left)")
print("right: \(view.safeAreaInsets.right)")
// borderView 자기 자신이 시작되는 y좌표의 위치 == safeArea가 시작되는 위치
print("borderView의 상단 y좌표: \(borderView.frame.origin.y)")
// == borderView.height
print("borderView의 전체 높이 (Height): \(borderView.safeAreaLayoutGuide.layoutFrame.maxY)")
// view (즉, 슈퍼뷰)가 끝나는 지점의 y좌표로부터 borderView가 끝나는 지점의 y좌표를 뺀 값은
// safeAreaInsets의 bottom 값과 같음
print("borderView의 하단 y좌표: \(view.frame.maxY - borderView.frame.maxY)")
signInButton.frame = CGRect(x: 20,
y: view.height-50-view.safeAreaInsets.bottom,
width: view.width-40,
height: 50)
}
viewDidLayoutSubviews()에서는 safeAreaInsets의 값이 설정되고, borderView의 y좌표를 통해 safe Area의 위치를 확인