원리→철학→실전 레시피→디버깅 순서로 정리한 종합 가이드다.
1) 인셋 3총사: 개념 정리
UIKit에서 인셋은 3종이 있다. 각각의 영향 범위를 혼동하면 의도와 다른 결과가 나온다.
UIButton.contentEdgeInsets
버튼 전체 콘텐츠 박스(= 이미지+타이틀) 바깥 여백. 탭 영역과도 연동될 수 있어 과대/과소 설정 주의.UIButton.imageEdgeInsets
콘텐츠 박스 내부에서 이미지(아이콘) 위치만 이동.UIButton.titleEdgeInsets
콘텐츠 박스 내부에서 타이틀 위치만 이동.
부호 규칙:
left에 양수를 주면 실제 콘텐츠는 오른쪽으로 밀린다.top양수는 아래로 이동.
2) 왜 UIBarButtonItem.imageInsets가 안 보일까?
순정 UIBarButtonItem에는 imageInsets 프로퍼티가 없다. 많은 코드베이스는 다음 둘 중 하나를 사용한다.
- 커스텀
UIButton을customView로 넣는 패턴
이 경우 인셋 조절은 버튼 인스턴스의imageEdgeInsets/contentEdgeInsets에 해야 한다. - 확장(Extension)으로
UIBarButtonItem.imageInsets흉내
내부에서customView as? UIButton으로 캐스팅해 위임하는 구현이 흔하다.
프로젝트에 따라 동작이 다를 수 있으니 내부 구현을 확인하는 습관이 필요하다.
핵심은 “바 버튼”이 아니라 “바 버튼의 커스텀 버튼”을 다룬다는 점이다.
3) SF Symbols 빠르게 짚기
- 크기·굵기·스케일은
UIImage.SymbolConfiguration또는UIButton.setPreferredSymbolConfiguration로 지정. - 렌더링 모드: 시스템 틴트를 쓰면 기본
template. 풀컬러를 쓰려면.alwaysOriginal. - 버전 호환: 예를 들어
qrcode는 iOS 13+,wallet.bifold/ticket은 iOS 16+. 폴백 전략이 필요하다.
let config = UIImage.SymbolConfiguration(pointSize: 17, weight: .regular, scale: .medium)
let image = UIImage(systemName: "qrcode", withConfiguration: config)
// 버튼에 우선 구성 적용
button.setPreferredSymbolConfiguration(config, forImageIn: .normal)
button.setImage(image, for: .normal)
4) UI 철학: 수학적 중앙 vs 시각적 중앙
아이콘의 수학적 중앙(bounding box의 중심)과 시각적 중앙(인간이 균형으로 인지하는 중심)은 다르다.
특히 프레임형 심볼(qrcode.viewfinder)이나 폭이 넓은 심볼(ticket)은 왼/오른쪽 무게감 차이가 커서 미세 보정이 필요하다.
원칙:
- 정렬 우선순위: 줄(baseline) 정렬 → 시각적 균형 → 픽셀 힌팅(반픽셀 경계 회피).
- 탭 목표 유지: 44×44pt 이상 보장. 인셋 조절로 탭 영역이 줄지 않도록 설계.
- 일관성: 모든 아이콘을 동일 수치로 맞추기보다, 각 심볼 특성별 소량 보정이 전체 일관성을 높인다.
5) 내비게이션 바에서의 권장 패턴
5.1 커스텀 버튼을 써서 확정 제어
func makeSymbolBarItem(systemName: String,
action: Selector,
target: AnyObject,
contentInsets: UIEdgeInsets = .zero,
imageInsets: UIEdgeInsets = .zero) -> UIBarButtonItem {
let btn = UIButton(type: .system)
btn.frame = CGRect(origin: .zero, size: CGSize(width: 44, height: 44)) // 탭 목표 보장
btn.setPreferredSymbolConfiguration(
UIImage.SymbolConfiguration(pointSize: 17, weight: .regular, scale: .medium),
forImageIn: .normal
)
btn.setImage(UIImage(systemName: systemName), for: .normal)
btn.contentEdgeInsets = contentInsets
btn.imageEdgeInsets = imageInsets
btn.addTarget(target, action: action, for: .touchUpInside)
return UIBarButtonItem(customView: btn)
}
- 장점: 팩토리/확장 내부 동작에 영향받지 않고, 크기·인셋·렌더링을 완전 통제.
- 주의:
rightBarButtonItems = [A, B]에서 첫 요소가 오른쪽 끝에 위치한다(시각적 순서 주의).
5.2 심볼별 기본 인셋 테이블(시작점)
enum NavGlyph: String { case myQR = "qrcode", scan = "qrcode.viewfinder", ticket = "ticket" }
func defaultInsets(for glyph: NavGlyph) -> UIEdgeInsets {
switch glyph {
case .myQR: return .zero // 정사각형 심볼: 추가 보정 거의 불필요
case .scan: return UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 6)
case .ticket: return UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 0)
}
}
이 값은 시작점일 뿐이며, 타이틀 길이·바 스타일(Large/Inline)·디바이스에 따라 ±2~6pt 범위에서 미세 조정한다.
6) contentEdgeInsets vs imageEdgeInsets 사용처
- 주변 숨 쉴 공간 확보 / 버튼 간 간격 조절 →
contentEdgeInsets - 시각 중심 보정(좌/우 미세 이동) →
imageEdgeInsets - 두 값을 동시에 크게 쓰면 탭 영역이 줄어들 수 있으니, 큰 간격 조절은
contentEdgeInsets, 미세 보정은imageEdgeInsets로 역할을 나눈다.
7) 배치별 고려 사항
7.1 Large Title / Inline / Compact 전환
- 세로 배치는 네비게이션 바가 관리한다.
top/bottom인셋은 보통 0이 가장 안전. - 필요한 경우라도 ±1~2pt 범위 내에서만 보정한다.
7.2 iPad / Mac Catalyst
- 가로 여백이 넓다. 다중 아이콘을 한 묶음으로 보이게 하려면 오른쪽 아이콘에
left인셋을 더 키워 내부로 끌어당기는 전략을 취한다.
7.3 RTL(오른쪽→왼쪽) 레이아웃
- 인셋은 물리적 left/right 기준이므로 RTL에서 방향을 뒤집는 도우미를 둔다.
extension UIEdgeInsets {
func mirrored(for view: UIView) -> UIEdgeInsets {
UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft
? UIEdgeInsets(top: top, left: right, bottom: bottom, right: left)
: self
}
}
8) “아이콘이 왜 안 바뀌지?” 원인과 해결
자주 보는 원인
- 바 버튼이
customView(UIButton) 기반인데UIBarButtonItem.image만 바꿈
→item.customView as? UIButton에setImage해야 반영. - 다른 코드가 나중에 덮어씀
→viewWillAppear/fetch()/델리게이트 콜백 뒤에 재적용. - 표시 순서 착시
→rightBarButtonItems의 첫 요소가 오른쪽 끝. 배열 순서를 재확인. - 구성 충돌
→ iOS 15+에서UIButton.Configuration와 인셋을 혼용하면 예측이 어려움. 한 축만 사용. - 렌더링 모드/틴트
→ 틴트색 대비가 약하면 바뀌지 않은 것처럼 보인다. 필요 시.alwaysOriginal.
확정 교체 스니펫
if let item = navigationItem.rightBarButtonItems?.first, // 배열 순서 주의
let btn = item.customView as? UIButton {
let cfg = UIImage.SymbolConfiguration(pointSize: 17, weight: .regular, scale: .medium)
btn.setPreferredSymbolConfiguration(cfg, forImageIn: .normal)
btn.setImage(UIImage(systemName: "qrcode"), for: .normal)
btn.tintColor = .label
btn.setNeedsLayout()
btn.layoutIfNeeded()
}
덮어쓰기 감시 로그
print("Before:", navigationItem.rightBarButtonItems as Any)
applyRightBarButtons()
print("After :", navigationItem.rightBarButtonItems as Any)
9) 접근성·제스처·안전성
- 탭 목표 44×44pt 확보:
customView프레임을 명시하거나 오토레이아웃 제약으로 보장. - VoiceOver 라벨:
accessibilityLabel/Traits설정. - 제스처 충돌 회피: 내비바 아이콘은 가능하면 버튼 하나로 단순화.
10) 픽셀 힌팅과 시각적 선명도
레티나에서도 반픽셀 경계에 걸리면 가장자리가 흐릿해 보인다.
인셋을 조절할 때 짝수 pt 위주로 미세 조정해 선명도를 확보한다.
11) 유지보수 전략
- 심볼별 기본 인셋 테이블을 코드로 관리하여 변경 이력을 남긴다.
- Size Class(Compact/Regular)·디바이스·플랫폼(iPad/Mac)별로 보정값을 분기.
- 버전 폴백: iOS 16+ 심볼은 대체 심볼/에셋 준비.
- 스냅샷 UI 테스트로 레그레션을 방지한다.
12) 실전 레시피: QR·스캐너 두 아이콘 배치
let myQR = makeSymbolBarItem(
systemName: "qrcode",
action: #selector(showMyQrCodeButtonSelected),
target: self,
contentInsets: .zero,
imageInsets: .zero // 정사각 심볼은 기본값으로 시작
)
let scan = makeSymbolBarItem(
systemName: "qrcode.viewfinder",
action: #selector(scanQRCodeButtonSelected),
target: self,
contentInsets: .zero,
imageInsets: UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 6) // 프레임형 심볼 보정
)
// 주의: 배열 첫 요소가 오른쪽 끝에 위치
navigationItem.rightBarButtonItems = [myQR, scan]
13) 좌측 아이콘: 앱 아이콘(장식)로 교체
let iv = UIImageView(image: UIImage(named: "AppIconNav")?.withRenderingMode(.alwaysOriginal))
iv.contentMode = .scaleAspectFit
iv.isUserInteractionEnabled = false
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.widthAnchor.constraint(equalToConstant: 28),
iv.heightAnchor.constraint(equalToConstant: 28)
])
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: iv)
// 좌측은 보통 인셋 0이 가장 안정적
14) 요약
imageInsets는 미세 정렬을 위한 도구다. 주변 간격/탭 영역은contentEdgeInsets로 조절한다.UIBarButtonItem은 내부customView(UIButton)를 직접 제어하는 패턴이 가장 예측 가능하다.- 시각적 중앙을 맞추기 위해 심볼별로 소량 보정하되, 탭 목표(44×44pt)·RTL·대체 심볼을 함께 고려한다.
- 덮어쓰기/구성 충돌/배열 순서 착시가 흔한 함정이며, 로그·스냅샷 테스트·버전 폴백으로 안정성을 높인다.
답글 남기기