계산기 앱을 만드는 과제 도중 오토레이아웃으로 작성한 키패드 화면이 디바이스에 따라 잘려 보이거나 적절해 보이지 않을 수도 있음을 발견했다.

위와 같은 현상이 발생하는 이유는 키패드를 나타내는 스택뷰의 너비와 높이를 고정했기 때문이다.
동적인 디바이스 크기에 대응하기 위해 오토레이아웃을 적용했지만, 잘못 적용하면 오히려 좋지 않은 레이아웃이 될 수 있다.
먼저 너비와 높이에 주었던 제약조건을 제거하고, superview의 leading, trailing, bottom으로부터 일정 거리를 띄워줘봤다.
그랬더니 위와 같은 결과물이 나왔다.
스택 뷰의 크기가 변함에 따라 각 버튼의 크기도 변경되는데, 버튼을 둥글게 만들어주는 cornerRadius 값은 고정되어 있어 완전한 동그라미가 나타나지 않는다.
또한 스택뷰의 비율이 고정되어 있지 않아, 각각의 버튼은 정사각형이 아닌 직사각형이 되어 cornerRadius 값 조정을 통해 완전한 원을 만들기 힘들다.
따라서 스택 뷰의 aspect ratio를 고정시키는 것이 필요하다는 결론에 도달했다.
스택 뷰의 비율을 고정시키기 위해서 먼저 centerX 제약조건을 통해 항상 가운데 위치하도록 해주고, 너비 제약조건을 superview의 width로부터 60을 뺀 값을 주었다. 그러면 leading, trailing에 30, -30씩 제약조건을 준 것과 같은 너비를 가지게 된다.
이후 높이 제약조건을 자기자신의 widthAnchor와 맞추었더니 정사각형 모양을 가지며 여러 디바이스에 대응 가능한 스택 뷰를 만들 수 있었다.
| TouchID 아이폰 | FaceID 아이폰 | 아이패드 |
|---|---|---|
![]() | ![]() | ![]() |
그런데 이렇게 될 경우 각 버튼의 크기가 각 디바이스마다 달라지기 때문에 일괄적인 cornerRadius값을 적용한다면 삐뚤빼뚤한 모양의 원형 버튼이 만들어진다.
cornerRadius를 적용해 정사각형을 정확한 원으로 만들기 위해서는 원의 반지름만큼의 값을 주면 된다.
따라서 각 버튼 frame의 width 또는 height에 나누기 2를 한 값을 cornerRadius에 할당하면 된다.
그러면 해당 값의 할당을 어느 시점에 해줘야 원하는 대로 동작할까?
버튼을 생성하는 시점에 함께 설정해주는 방식은 아무런 영향도 끼치지 못한다. 버튼을 생성하는 시점의 frame값과 스택뷰에 의해 조정된 크기의 frame값은 다르기 때문이다.
스택 뷰가 자신의 서브뷰 위치를 모두 조정하고, 그 크기를 알게 되는 시점에야 정확힌 radius 값을 줄 수 있다. (또는 버튼이 4개이기 때문에 디바이스 너비에서 각 패딩값을 제외해 버튼의 크기를 미리 계산하는 방법도 있다. 그러나 이는 확장성이 떨어지는 방법이고, 디바이스를 옆으로 돌릴 경우 대응할 수 없기 때문에 추천하지 않는다.)
오토레이아웃 제약조건을 작성하는 configureUI 코드에서 작성하는 것도 영향을 끼치지 못한다.
이런 제약조건을 가진 오토레이아웃을 사용할 것이라고 정의하기만 하는 순간이고, 실제 오토레이아웃 엔진의 실행은 다른 시점에서 일어나기 때문이다.
이는 viewWillLayoutSubviews -> layoutSubviews -> viewDidLayoutSubviews 의 순서로 일어난다.
viewWillLayoutSubviews의 시점에는 아직 키패드 스택뷰의 레이아웃이 계산되지 않은 시점이고, viewDidLayoutSubviews의 시점에는 이미 레이아웃 과정이 끝났기 때문에 cornerRadius 값을 할당해도 레이아웃에 영향을 미치지 못한다.
따라서 우리가 cornerRadius를 할당해야 하는 시점은 layoutSubviews 이다.
해당 layoutSubviews는 superview -> subview의 순서대로 연쇄적으로 호출된다.
따라서 스택 뷰의 레이아웃이 계산된 이후 버튼의 layoutSubviews 함수가 불릴 것이다.
버튼의 layoutSubviews가 호출되는 과정에 알맞은 cornerRadius를 할당하기 위해 아래와 같은 코드를 작성해 사용하였다.
final class CircularButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
let radius: CGFloat = self.bounds.size.width / 2.0
self.layer.cornerRadius = radius
}
}