안녕하세요. Deah 입니다. (제 닉네임은 데아라고 읽어요.)
iOS 개발을 시작한 후로 잔잔한 오류를 여러 번 겪었지만 글로 남기지는 못했는데 이번에 겪은 오류는 몰랐던 내용을 알게 되어 글로 남겨보려고 합니다.
제목에서 보셨듯이 UILabel에서 cornerRadius가 적용이 안 되는 문제입니다.
스토리보드에서 TableView를 다루는 연습을 하면서 아래 예시와 같이 서로 다른 두 종류의 커스텀 셀을 Travel
인스턴스의 ad
프로퍼티 값(Bool)에 따라 렌더링을 해주려고 했습니다.
struct Travel {
let title: String
let description: String?
let travel_image: String?
let grade: Double?
let save: Int?
var like: Bool?
let ad: Bool
}
// 인스턴스 예시
Travel(title: "남산타워",
description: "남산의 정기를 느낄 수 있는 서울의 대표 랜드마크",
travel_image: "https://images.unsplash.com/생략",
grade: 4.8,
save: 6932,
like: false,
ad: false)
중간에 삽입되어 있는 광고 커스텀 셀의 경우, 레이어와 오른쪽 상단 광고 뱃지의 가장자리가 모두 둥글게 라운드 처리가 되어있어 이를 구현하는 과정 중에 cornerRadius를 적용했으나 빌드 후 시뮬레이터 상에서 적용되지 않는 오류가 발생했습니다. 🤯
import UIKit
class CityAdTableViewCell: UITableViewCell {
static let identifier = "CityAdTableViewCell"
@IBOutlet var adTextLabel: UILabel!
@IBOutlet var adBadgeLabel: UILabel!
func configureCellUI() {
adTextLabel.font = .boldSystemFont(ofSize: 14)
adTextLabel.textColor = .black
adTextLabel.textAlignment = .center
adTextLabel.numberOfLines = 0
adTextLabel.randomBackgroundColor()
adTextLabel.layer.cornerRadius = 10 // ➔ 적용 안 됨!
adBadgeLabel.textAlignment = .center
adBadgeLabel.backgroundColor = .white
adBadgeLabel.layer.cornerRadius = 4 // ➔ 적용 안 됨!
}
// 생략
}
검색해보니 아주 간단한(?) 해결 방법이 있었습니다.
cornerRadius
를 적용하기 전 UILabel의 clipsToBounds
인스턴스 프로퍼티를 true
로 설정해주면 된다고 합니다.
UILabel.clipsToBounds = true
clipsToBounds
A Boolean value that determines whether subviews are confined to the bounds of the view.
(오류 해결 코드는 간단하지만 이해는 간단하지 않은 개념 등장..!)
clipsToBounds는 UIView에 속해있는 인스턴스 프로퍼티입니다.
subview가 view의 경계(boundary)를 넘어선 경우에 view를 넘어서 그릴 것인지를 말 것인지를 설정하는 인스턴스로, 기본값은 false입니다. true로 설정할 경우에는 subview가 view 경계에 맞게 잘려지게 됩니다.
다시 말해, clipsToBounds가 true로 설정됐을 경우 view의 경계를 벗어난 subview 부분은 화면에 보이지 않게 잘린다는 의미!
blueView = view
greenView = subview
blueView.clipsToBounds = true
위 예제에서 파란색 정사각형이 view, 초록색 직사각형이 subview라고 했을 때
view의 clipsToBounds 값을 true로 바꿔주면, 예제에서 아래 그림처럼 view의 경계를 벗어난 subview의 부분들이 잘려서 안 보이게 됩니다. 반대로 view의 clipsToBounds 값이 false일 경우에는 예제에서 위에 있는 그림처럼 subview가 잘리지 않고 그대로 나타납니다.
(clipsToBounds는 상위 view에서 설정해줘야 한다는 점에 유의해야 해요!)
적용 전 | 적용 후 |
---|---|
![]() | ![]() |
func configureCellUI() {
adTextLabel.font = .boldSystemFont(ofSize: 14)
adTextLabel.textColor = .black
adTextLabel.textAlignment = .center
adTextLabel.numberOfLines = 0
adTextLabel.randomBackgroundColor()
adTextLabel.clipsToBounds = true // ➔ clipsToBounds 적용!
adTextLabel.layer.cornerRadius = 10
adBadgeLabel.textAlignment = .center
adBadgeLabel.backgroundColor = .white
adBadgeLabel.clipsToBounds = true // ➔ clipsToBounds 적용!
adBadgeLabel.layer.cornerRadius = 4
}
광고 셀 내부에 있는 텍스트 레이블에 clipsToBounds를 true
로 설정했더니 드디어 cornerRadius가 잘 적용된 것을 볼 수 있습니다! (이고잉 님이시라면 이 때 박수를 치라고 하셨겠죠... 짝짝짝짝짝.)
clipsToBounds를 찾아보며 이해한 내용을 바탕으로 작업했던 스토리보드와 코드를 다시 살펴봤는데 딱히 이거다! 하는 이유는 아직 찾지 못했습니다.😨 아무래도 XIB로 커스텀 셀을 만들면서 사용했던 레이블의 순서(계층) 관계를 잘 파악하지 못하고 있는 거 같다고 짐작을 해보고 있어요.
최근 다양한 뷰 컨트롤러와 하나의 씬에서도 다양한 뷰를 조합해 다루게 되면서 view, subview, superview 등 뷰의 계층 개념이 아직 잘 잡혀있지 않아 이 부분에 대한 학습이 필요한 거 같다고 느끼는 거 같습니다.
그리고 clipsToBounds를 찾다보니 비슷한 개념으로 masksToBounds 프로퍼티 이야기도 많이 나와서 조만간 이 개념도 같이 묶어서 다시 정확한 원인을 찾아보는 것으로 도전!
더 디벨롭될 내용이 있다면 차근차근 추가해보겠습니다. (찡긋)
(+) 2024. 06. 01 내용 추가
아래부터는 추가 학습 이후 덧붙여진 내용입니다!
clipsToBounds 개념을 익힌 후, 위 예제처럼 이전보다 조금 더 복잡해진 커스텀 셀을 연습하던 중 (또!!!) cornerRadius 적용이 안 되는 이슈를 겪었습니다. 지저스... clipsToBounds와 true만 있다면 어떤 뷰 객체든 둥글게 할 수 있다고 믿었는데요...
일단 예제처럼 커스텀 셀을 만들기 위해서는 1차적으로 아래와 같이 생각하고 구현을 시작했습니다.
- ImageView 가장자리(모서리)에 cornerRadius를 적용한다.
- cornerRadius를 각 모서리별로 설정할 수 있도록 한다.
- cornerRadius를 적용하기 위해서 clipsToBounds를 true로 설정한다.
- clipsToBounds가 true일 때, ImageView 바깥으로 그림자 효과를 줄 수 없으므로 그림자 효과를 위한 별도의 UIView를 깔아준다.
- UIView에도 각 모서리별로 cornerRadius를 적용한다.
제가 만든 커스텀 셀의 XIB상에서 계층 구조와 초기 설정 코드를 살펴보면 대략 이렇습니다.
ContentView(기본) - 이미지 크기와 같은 UIView (= backView, 그림자용) - ImageView (이미지) - TextLabel (상,하단 텍스트)
import UIKit
class 커스텀셀클래스 {
@IBOutlet var backView: UIView!
@IBOutlet var imageView: UIImageView!
@IBOutlet var nameLabel: UILabel!
@IBOutlet var explainLabel: UILabel!
// 셀 UI 초기설정
func configureCellUI() {
// 가장 하위 layerView
// shadow 처리
backView.backgroundColor = .clear
backView.layer.shadowColor = UIColor.black.cgColor
backView.layer.shadowOffset = CGSize(width: 4, height: 4)
backView.layer.shadowRadius = 4
backView.layer.shadowOpacity = 0.4
// 이미지 뷰
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true // ➔ 여기에 clipsToBounds 적용!
imageView.layer.cornerRadius = 16
imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner]
// 도시 이름 레이블
nameLabel.textColor = .white
nameLabel.font = .boldSystemFont(ofSize: 20)
// 도시 설명 레이블
explainLabel.textColor = .white
explainLabel.font = .systemFont(ofSize: 14, weight: .medium)
let labelBgColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
explainLabel.backgroundColor = labelBgColor
explainLabel.layer.cornerRadius = 16
explainLabel.layer.maskedCorners = .layerMaxXMaxYCorner
}
}
그리고 빌드 후 시뮬레이터를 확인해보면...
왼쪽 상단은 cornerRadius가 잘 적용된 걸 볼 수 있습니다. but... 오른쪽 아래는 처참합니다.
🤯 : "나 분명 cornerRadius 주고싶은 요소들의 최상위 view인 ImageView한테
clipsToBounds = true
설정했는데? "
👩🏻💻 Dev : 나 이거 적용할게.
📱 App : 응. 안 될게.
clipsToBounds를 학습하면서 기본값(default)은 false라고 했습니다.
하지만 모든 View가 그럴까요?
clipsToBounds 공식 문서에 보면 아래와 같이 설명되어 있습니다.
Discussion
The default value is false. Some subclasses of UIView, like UIScrollView, override the default value to true.
(기본값은 false입니다. UIScrollView와 같은 UIView의 일부 하위 클래스는 기본값을 true로 재정의합니다.)
UIView의 일부 하위 클래스는 기본값을 true로 재정의합니다...
UIView의 일부 하위 클래스는 기본값을 true로 재정의합니다...
UIView의 일부 하위 클래스는 기본값을 true로 재정의합니다...
모든 View의 clipsToBounds 기본값이 false가 아니었습니다.
제가 커스텀 셀에서 사용하던 요소의 clipsToBounds 기본값을 각각 프린트로 확인해보면,
UIImageView는 기본값이 true, UILabel은 기본값이 false 입니다.
print(imageView.clipsToBounds) // true
print(explainLabel.clipsToBounds) // false
제가 코드에서 imageView에 clipsToBounds = true
를 설정했더라도 하단 텍스트 레이블에 같이 적용이 되지 않았던 이유는 UILabel의 clipsToBounds가 false였기 때문에 cornerRadius가 적용되지 않았던 거에요. 😭
func configureCellUI() {
// 가장 하위 layerView
// shadow 처리
backView.backgroundColor = .clear
backView.layer.shadowColor = UIColor.black.cgColor
backView.layer.shadowOffset = CGSize(width: 4, height: 4)
backView.layer.shadowRadius = 4
backView.layer.shadowOpacity = 0.4
// 이미지 뷰
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 16 // ➔ clipsToBounds 없이 바로 cornerRadius 적용!
imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner]
// 도시 이름 레이블
nameLabel.textColor = .white
nameLabel.font = .boldSystemFont(ofSize: 20)
// 도시 설명 레이블
explainLabel.textColor = .white
explainLabel.font = .systemFont(ofSize: 14, weight: .medium)
let labelBgColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
explainLabel.backgroundColor = labelBgColor
explainLabel.clipsToBounds = true // ➔ clipsToBounds true 적용!
explainLabel.layer.cornerRadius = 16
explainLabel.layer.maskedCorners = .layerMaxXMaxYCorner
}
clipsToBounds가 기본으로 true인 imageView에서는 해당 코드를 삭제하고,
explainLabel에 clipsToBounds를 true로 설정한 뒤 다시 빌드를 하면 성공!✨
저는 iOS를 배우기 전 1년간 JavaScript 언어를 공부했습니다.
JavaScript는 이벤트 위임(Event Delegation)이라는 개념이 있는데, 간단히 말하면 모든 요소마다 매번 이벤트를 할당하지 않고 상위 요소 하나에 이벤트를 할당하여 하위 요소들 한 번에 핸들링 할 수 있도록 하는 개념입니다.
iOS를 배우면서 App Delegate, Scene Delegate 등의 개념을 배우다보니 자연스럽게 JavaScript의 이벤트 위임처럼 Swift에서도 코드상 상위 요소에 무언갈 적용하면 하위 요소에도 같이 적용될 거라고 생각했던 거 같습니다. (^^...;;;) 너무나 안일하고 바보같았던...!
이번 트러블 슈팅을 통해 얻은 것!
이번 이슈는 제가 공식문서를 꼼꼼히 읽지 않아 생긴 일 같다고 생각합니다.
코 앞에 정답이 있었는데도 이유를 못 찾고...
이제부터 조금이라도 더 꼼꼼하게 공식문서를 들여다봐야겠습니다. (with. 영어 울렁증 극복)
처음에는 요소의 clipsToBounds를 프린트로 찍어서 확인해야겠다는 생각을 못했습니다.
멘토님께 질문드리니 그제서야 프린트로 확인해볼걸! 하는 생각이 들었던...
👨🏫 "프린트 찍어보셨나요?"
👩🏻💻❓"아 맞다 프린트"
JavaScript를 주 언어로 쓸 때는 무조건 콘솔 찍어보는 습관이 있었는데, 새로운 언어 배운다고 머릿 속이 같이 초기화된 건지?
왜 안 되지? 왜 되는거지? 생각하기에 앞서 안 되거나 궁금한 게 생기면 프린트로 확인하는 습관을 다시 길러야겠습니다!
masksToBounds와 maskedCorners에 대해서도 다뤄보고 싶었는데 이미 글이 너무 길어져서 패스!
추후에 시간이 되면 따로 포스팅을 해보도록 하겠습니다.
모쪼록 저와 같은 이슈를 겪으시거나 이 글을 읽으시는 분들께 조금이라도 도움이 되었길 바랍니다 :)
(*오류를 발견하시면 댓글로 피드백 부탁드려요!)
참고자료
Apple Developers - clipsToBounds
Microsoft - UIView.ClipsToBounds
Why is clipsToBounds behaving opposite my expectations?