문제상황
어느 날, 아래의 디자인 요구사항이 들어왔다.
“Corner Radius를 왼쪽 위와 아래는 2로, 오른쪽 위와 아래는 8로 해주세요.”
(위 영상은 눈에 잘 보이라고 왼쪽 위-아래 12, 오른쪽 위-아래 32로 설정했다.)
이전 편에서 썼듯이 CACornerMask와 UIBezierPath, CAShapeLayer를 통해 개별처리를 할 수 있다는 것을 알았다.
근데 셀 안에 들어가니까 얘기가 달라졌다.
해결과정
1. 생성자 시점에서 configureCornerRadius() 호출
뷰가 그려질 때 처음부터 잡히면 좋겠거련만, 그게 잘 안된다.
코드는 다음과 같다.
final class TimeSlotTableViewCell: UITableViewCell {
override init(
style: UITableViewCell.CellStyle,
reuseIdentifier: String?
) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureCornerRadius()
}
private func configureCornerRadius() {
eventDetailView.layer.cornerRadius = 32
eventDetailView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
eventDetailView.clipsToBounds = true
let maskPath = UIBezierPath(
roundedRect: eventDetailView.bounds,
byRoundingCorners: [.topLeft, .bottomLeft],
cornerRadii: CGSize(width: 12, height: 12)
)
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
eventDetailView.layer.mask = maskLayer
}
}

셀 생성자에 configureCornerRadius를 두어, 생성 시점에 바로 mask 작업을 하면 아래처럼 셀이 안 보이게 된다.
아마 eventDetailView 뷰의 레이아웃이 완전히 이루어지기 전에 적용되므로 올바른 bounds 값을 반영하지 못해 뷰가 제대로 보이지 않는 것 같다. bounds가 0이기 때문인듯.
2-1. layoutSubViews를 통해 bounds가 잡힌 후 Radius 호출
먼저 layoutSubViews의 설명은 다음과 같다.
하위 뷰의 자동 크기 조정 및 제약 기반 동작이 원하는 동작을 제공하지 않는 경우에만 이 메서드를 재정의해야 합니다. 구현을 사용하여 하위 뷰의 프레임 사각형을 직접 설정할 수 있습니다.
뷰가 그려져있더라도, [setNeedsLayout(),](https://developer.apple.com/documentation/uikit/uiview/setneedslayout()) [layoutIfNeeded()](https://developer.apple.com/documentation/uikit/uiview/layoutifneeded())
이 두 메소드가 호출돼서 레이아웃이 잡힐 때마다 동작한다.
나는 layoutSubViews 안에 configureCornerRadius을 두어 처리했다.
뷰가 그려지고 난 후, 즉 bounds가 잡히고나면 configureCornerRadius를 호출하게끔 했다.
참고로 didSetCornerRadius는 layoutSubviews가 뷰를 그리고 난 후에도 계속 업데이트 될 수 있기 때문에 비효율적인 동작을 위해서 guard문을 두어 처음에만 그려지게 하였다.
final class TimeSlotTableViewCell: UITableViewCell {
private var didSetCornerRadius = false
override func layoutSubviews() {
super.layoutSubviews()
setupCornerRadiusIfNeeded()
}
private func setupCornerRadiusIfNeeded() {
guard !didSetCornerRadius else { return }
configureCornerRadius()
didSetCornerRadius = true
}
...
}
그랬더니 1번과 똑같이 동작했다.
이유를 생각해보았다.
처음에 뷰가 생성자에 의해 그려지면 layoutSubviews가 동작할 거고, setupCornerRadiusIfNeeded가 호출 될 것이다.
그럼 생성자 호출가 동일하다는 것이다.
뷰가 그려질 때 configureCornerRadius를 호출하게 되니까 결국 뷰가 제대로 안 보이게 되는 것이다.
2-2. setupCornerRadiusIfNeeded에 eventDetailView.bounds != .zero 조건 추가
guard 문에 eventDetailView.bounds != .zero
를 추가해서, 처음 뷰가 그려질 때는 configureCornerRadius가 동작하지 않게끔 뷰가 안 그려지는 일은 없게 설정했다.
그랬더니 처음에는 Radius가 8인데,
셀을 누르고 있거나, 클릭하면 그제서야 Radius가 2, 2, 8, 8로 잡히는 것이다.
셀을 누른다거나, 한 번 스크롤/터치 액션이 발생해서 레이아웃이 갱신되면(layoutSubviews가 다시 호출) 그제서야 layer.mask가 최종적으로 제대로 먹으면서, 왼쪽 모서리가 2, 오른쪽 모서리가 8짜리 둥근 모서리로 보이게 되는 것이다.
즉, “처음엔 전부 8로 보이는 이유”는
- 초기 레이아웃 시점에 cornerRadius(=8) 설정이 먼저 적용
- 마스크는 적용되었지만, bounds가 0이거나 아직 확정되지 않아 눈에 띄지 않다가, 실제 크기가 잡힌 뒤(즉, 터치/재활용으로 layoutSubviews가 다시 호출) 마스크가 본격적으로 적용되어 “2, 2, 8, 8”로 보이게 되는 것이라 생각했다.
추가로 알게 된 게, Cell에는 setHighlighted라는 메소드와 setSelected 메소드가 있다.
그래서 셀이 눌리거나 클릭될 때 레이아웃을 새로 잡을 필요성이 생겨서 [setNeedsLayout(),](https://developer.apple.com/documentation/uikit/uiview/setneedslayout()) [layoutIfNeeded()](https://developer.apple.com/documentation/uikit/uiview/layoutifneeded())
가 동작하고, 이때 layoutSubviews가 호출돼 Radius가 후에 보였던 것이다.
3. 비동기 호출로 뷰의 bounds가 설정될 때까지 재귀적으로 확인
setupCornerRadiusIfNeeded
메서드를 수정하여 뷰의 bounds가 설정될 때까지 재귀적으로 확인하도록 한다. DispatchQueue.main.async
를 사용하여 다음 런 루프에서 다시 시도하면 레이아웃이 완료된 후에 경계 값이 설정될 거라 생각했다.
final class TimeSlotTableViewCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
setupCornerRadiusIfNeeded()
}
private func setupCornerRadiusIfNeeded() {
guard !didSetCornerRadius else { return }
if **eventDetailView.bounds != .zero** {
configureCornerRadius()
didSetCornerRadius = true
} else {
DispatchQueue.main.async { [weak self] in
self?.**setupCornerRadiusIfNeeded**() // 여기가 재귀임!!
}
}
}
}
처음 뷰가 그려질 때 eventDetailView.bounds가 0이어서 guard에 막히면, DispatchQueue.main.async로 다음 Run Loop에 setupCornerRadiusIfNeeded를 호출하도록 하는 것이다. 이렇게 하면 뷰의 레이아웃이 완료되어 eventDetailView.bounds가 0이 아닌 상태가 되어야 한다.

처음에 비동기라 계속 재귀가 누적되면 어쩌지 걱정했는데 재귀호출 1번만에 되는 것을 확인했다.
참고로 4번이나 호출된 건 layoutSubviews가 여러 번 호출되기 때문이다.
그래서 Guard didSetCornerRadius를 넣었기도 했음
최종 결과물 ~~

배운 점
셀의 최종 프레임이 결정되는 시점에, 즉 Auto Layout이 적용되어 eventDetailView의 bounds가 확정된 후에 configureCornerRadius를 호출해야 한다. 일반적으로 이는 layoutSubviews에서 이루어진다.
셀 생성자나 awakeFromNib에서는 아직 Auto Layout에 의한 최종 레이아웃이 반영되지 않아 bounds가 0이므로, 원하는 모서리 반경(왼쪽 위/아래 2, 오른쪽 위/아래 8)을 적용하기에 적절하지 않다.
따라서, layoutSubviews에서 eventDetailView의 bounds가 0이 아닌 것을 확인한 후 한 번만 configureCornerRadius를 호출하는 방식이 올바르다.
참조 링크
https://developer.apple.com/documentation/uikit/uiview/layoutsubviews()
https://developer.apple.com/documentation/uikit/uiview/setneedslayout()
https://developer.apple.com/documentation/uikit/uiview/layoutifneeded()
'📱 iOS > UIKit' 카테고리의 다른 글
[문제해결] UITableViewCell 셀 내부 제스처가 스크롤을 막는 문제 해결: UIGestureRecognizerDelegate 활용 (0) | 2025.03.01 |
---|---|
[UIKit] 뷰의 Corner Radius 각각 다르게 처리하기 (with CACornerMask, UIBezierPath, CAShapeLayer) (0) | 2025.02.28 |
[UIKit] UIMenu 사용기 (1) | 2024.12.10 |
[UIKit] CollectionViewCell 드래그 앤 드랍 구현하기 (0) | 2024.12.01 |
[UIKit] iOS 15.0 이상에서 UIButton 안에 있는 이미지 사이즈 조절하기 (3) | 2024.11.15 |