
아래의 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 값은 기본적으로 0viewWillLayoutSubviews() → 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의 위치를 확인