(UIBarButtonItem / UIButton / SF Symbols 정밀 가이드)
이 문서는 내비게이션 바 우측/좌측 아이콘의 시각적 정렬(Optical Alignment)·탭 영역 보장·OS 호환성을 목표로, imageInsets를 중심으로 다룹니다. 특히 UIBarButtonItem에서 흔히 겪는 “왼쪽이 너무 붙는다/멀다”, “스캐너 아이콘은 예쁜데 QR 아이콘은 쏠려 보인다” 같은 문제를 원인→원칙→실전 레시피 순서로 해결합니다.
1) 기본 개념과 오해 풀기
1.1 UIKit에서의 인셋 3종
UIButton.contentEdgeInsets
버튼 전체 콘텐츠(= image+title) 박스를 안쪽/바깥쪽으로 이동·확장합니다.UIButton.imageEdgeInsets
콘텐츠 박스 안에서 이미지만 상대 이동합니다.UIButton.titleEdgeInsets
콘텐츠 박스 안에서 타이틀만 상대 이동합니다.
인셋의 부호 규칙: 양수 Left 인셋은 콘텐츠를 오른쪽으로 밀어냅니다. (Top 양수는 아래로)
1.2 UIBarButtonItem에는 원래 imageInsets가 없다
- 순정 UIKit의
UIBarButtonItem에는imageInsets프로퍼티가 없습니다. - 많은 코드베이스가 커스텀
UIButton을customView로 래핑하거나,
**확장(Extension)으로imageInsets**을 추가해 내부 버튼의imageEdgeInsets에 위임합니다. - 따라서 다음 둘 중 하나로 동작합니다:
UIBarButtonItem.customView← (UIButton) ←imageEdgeInsets조절- 팀 공용 확장:
barButton.imageInsets = ...→ 내부적으로 customView의imageEdgeInsets로 전달
현재 프로젝트처럼
barButton.imageInsets = ...이 컴파일된다면, 내부에 이런 확장이 들어있다고 봐도 됩니다.
2) 왜 인셋이 필요한가? — UI 철학
2.1 “수학적 중앙”과 “시각적 중앙”의 차이
- SF Symbols는 글리프마다 여백·무게 중심이 다릅니다.
예:qrcode(정사각형) vsqrcode.viewfinder(프레임 포함) - 같은 크기라도 왼쪽/오른쪽이 시각적으로 달라 보일 수 있어 약간의 인셋 보정이 필요합니다.
2.2 탭 목표(44×44pt)와 인셋의 균형
- 인셋으로 이미지의 시각적 위치만 다듬되, 탭 가능 영역은 항상 44×44pt 이상 유지하세요.
너무 큰 인셋(특히 음수)은 탭 영역 축소나 클리핑을 불러옵니다.
2.3 일관성(Consistency) > 동일성(Uniformity)
- 서로 다른 아이콘을 “완전히 같은 수치”로 맞추는 것이 정답은 아닙니다.
- 줄 맞춤(leading/trailing baseline), 시각적 균형, 맥락의 의미를 먼저 두고,
수치를 “약간씩 다르게” 가져가도 전체가 깔끔해 보이면 OK입니다.
3) 실전: 내비게이션 바에서의 인셋 운용
3.1 추천 기본값 (iPhone, iOS 15+ · Inline Title 기준)
- 우측 아이콘 2개 배치 예시
- 왼쪽 아이콘(내 QR):
contentEdgeInsets = {0, 0, 0, 0},imageEdgeInsets = {0, 0, 0, 0} - 오른쪽 아이콘(스캐너):
imageEdgeInsets = {0, +12, 0, +6}
→ 오른쪽 아이콘을 약간 안쪽으로 끌어당겨 좌우 간격과 시각적 그룹 형성
- 왼쪽 아이콘(내 QR):
- 좌측 아이콘(앱 아이콘·블록키)
imageEdgeInsets는 가급적 0 유지 → 시스템이 leading 마진 관리- 필요 시 아주 소량
{0, +2, 0, 0}정도로 미세 조정
위 수치는 시작점입니다. 실제 앱 타이틀 길이, 바 스타일(large vs inline), Symbol 종류에 따라 ±2~6pt 범위에서 조절하세요.
3.2 Large Title / Compact 상태 전환
- iOS 15+의
UINavigationBarAppearance로 Large/Inline/Compact 간 전환 시,
세로 레이아웃은 바가 처리하고,imageEdgeInsets.top/bottom은 보통 0 유지가 가장 안전합니다. - 수직 보정이 필요해도 ±1~2pt 이내로 제한하세요.
3.3 iPad / Mac Catalyst
- 좌우 여백이 넉넉합니다. 우측 다중 아이콘의 경우 아이콘 간격을 더 좁히는 인셋이 필요할 수 있습니다.
예:{0, +10, 0, +4}→ 툴바형 레이아웃에서 “한 묶음”으로 보이게.
3.4 RTL(오른쪽→왼쪽) 언어
- 인셋은 논리 방향(leading/trailing) 기준이 아니라 물리 방향(left/right) 기준입니다.
- RTL 대응을 위해 미러링 도우미를 쓰세요.
extension UIEdgeInsets {
func mirrored(for view: UIView) -> UIEdgeInsets {
if UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft {
return UIEdgeInsets(top: top, left: right, bottom: bottom, right: left)
} else { return self }
}
}
4) 구현 패턴
4.1 커스텀 버튼 기반 바 버튼 (권장)
func makeSymbolBarItem(
systemName: String,
action: Selector,
target: AnyObject,
imagePointSize: CGFloat = 17,
imageWeight: UIImage.SymbolWeight = .regular,
imageScale: UIImage.SymbolScale = .medium,
contentInsets: UIEdgeInsets = .zero,
imageInsets: UIEdgeInsets = .zero
) -> UIBarButtonItem {
let button = UIButton(type: .system)
let config = UIImage.SymbolConfiguration(pointSize: imagePointSize, weight: imageWeight, scale: imageScale)
let img = UIImage(systemName: systemName, withConfiguration: config)
button.setImage(img, for: .normal)
button.contentEdgeInsets = contentInsets
button.imageEdgeInsets = imageInsets
button.frame = CGRect(origin: .zero, size: CGSize(width: 44, height: 44)) // 탭 목표 보장
button.addTarget(target, action: action, for: .touchUpInside)
return UIBarButtonItem(customView: button)
}
4.2 UIBarButtonItem.imageInsets 확장 (프로젝트 스타일)
private var _ImageInsetsKey: UInt8 = 0
extension UIBarButtonItem {
var imageInsets: UIEdgeInsets {
get { objc_getAssociatedObject(self, &_ImageInsetsKey) as? UIEdgeInsets ?? .zero }
set {
objc_setAssociatedObject(self, &_ImageInsetsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if let b = customView as? UIButton {
b.imageEdgeInsets = newValue
}
}
}
}
이렇게 해두면
barButton.imageInsets = ...로 간단히 조절 가능. 단,customView가 버튼이 아닐 때는 조심.
4.3 SF Symbols + 인셋 튜닝 도우미
- 글리프마다 폭이 다르니 기본 오프셋 테이블을 만들어 두면 유지보수에 유리합니다.
enum NavGlyph: String {
case myQR = "qrcode"
case scan = "qrcode.viewfinder"
case ticket = "ticket"
}
func defaultImageInsets(for glyph: NavGlyph) -> UIEdgeInsets {
switch glyph {
case .myQR: return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) // 정사각: 기본
case .scan: return UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 6) // 프레임형: 살짝 내부로
case .ticket: return UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 0) // 폭 넓은 글리프: 미세
}
}
5) 섬세한 디테일
5.1 템플릿 vs 오리지널 렌더링
- 내비바 틴트 색상을 쓰려면 Template(기본).
- 풀컬러 아이콘(앱 아이콘 등)은
.alwaysOriginal로 설정하고 인셋만 조절하세요.
5.2 contentEdgeInsets vs imageEdgeInsets
- 간격 벌리기(아이콘 주변 숨 쉴 공간) →
contentEdgeInsets - 시각 중심 보정(왼쪽/오른쪽 조금만 이동) →
imageEdgeInsets - 두 값을 한꺼번에 크게 잡으면 탭 목표가 줄어들 수 있으니 주의.
5.3 버튼 구성(UIButton.Configuration)과의 충돌
- iOS 15+
UIButton.Configuration는contentInsets,imagePadding등을 제공합니다. imageEdgeInsets와 동시에 쓰면 예측이 어려워질 수 있으므로,
하나의 축(Configuration 기반 또는 EdgeInsets 기반)만 택하는 걸 권장합니다.
내비바 커스텀뷰에서는 EdgeInsets 기반이 관리가 쉽습니다.
5.4 alignmentRectInsets(이미지 자체의 정렬 사각형)
- 일부 커스텀 이미지(PDF)는 정렬 사각형이 비대칭일 수 있습니다.
- 이 경우 버튼 인셋이 아니라 이미지 에셋 자체를 손보는 편이 낫습니다.
(여백 통일, 아트보드 좌표 정리)
5.5 대각선 스냅(픽셀 힌팅)
- 1pt 단위 인셋이 레티나에서 반픽셀 경계를 타면 흐릿해 보일 수 있습니다.
- 수치 조정 시 짝수 pt 위주로 미세 조절해 보는 것도 방법입니다.
6) 접근성·제스처·안전성
6.1 탭 영역 보장
customView의frame을 명시적으로 44×44pt 이상으로 잡으세요.- 인셋 때문에 보이는 이미지가 작아져도 상관없지만, 터치 타겟은 줄지 않게.
6.2 VoiceOver 라벨
barButton.accessibilityLabel = "My QR Code"
barButton.accessibilityTraits = .button
6.3 UIGestureRecognizer와의 상호작용
customView위에 제스처를 추가하면 터치 전달 순서가 복잡해질 수 있습니다.- 내비바 아이콘은 버튼 하나로 단순 유지하는 게 안전합니다.
7) 디버깅·튜닝 체크리스트
- 런타임 뷰 계층 확인
print(navigationController.view.debugDescription) - 실제 이미지 렌더 크기 확인
let cfg = UIImage.SymbolConfiguration(pointSize: 17, weight: .regular) let img = UIImage(systemName: "qrcode", withConfiguration: cfg) print(img?.size ?? .zero) - RTL 시뮬레이션
iOS 설정 → 언어·지역 → 아랍어/히브리어 추가 → 우선순위 올려 테스트 - Large/Inline/Compact 상태 전환
스크롤로 LargeTitle 토글, 회전(세로/가로), iPad 멀티윈도우 - 손가락 테스트
엄지로 빠르게 탭해도 잘 눌리는지(44×44pt 보장)
8) “딱 이 케이스” 레시피 (당신의 화면에 바로 쓰는 값)
8.1 우측: “내 QR(qrcode)” + “스캐너(qrcode.viewfinder)”
let myQr = makeSymbolBarItem(
systemName: "qrcode",
action: #selector(showMyQrCodeButtonSelected),
target: self,
imagePointSize: 17,
imageWeight: .regular,
imageScale: .medium,
contentInsets: .zero,
imageInsets: .zero
)
let scan = makeSymbolBarItem(
systemName: "qrcode.viewfinder",
action: #selector(scanQRCodeButtonSelected),
target: self,
imagePointSize: 17,
imageWeight: .regular,
imageScale: .medium,
contentInsets: .zero,
imageInsets: UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 6) // 시작점
)
tokensViewController.navigationItem.rightBarButtonItems = [myQr, scan]
8.2 좌측: 앱 아이콘(클릭 불가)
let iv = UIImageView(image: appIconImage()?.withRenderingMode(.alwaysOriginal))
iv.contentMode = .scaleAspectFit
iv.isUserInteractionEnabled = false
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.widthAnchor.constraint(equalToConstant: 28),
iv.heightAnchor.constraint(equalToConstant: 28)
])
tokensViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: iv)
// 일반적으로 좌측 아이콘은 imageInsets 0 유지 권장
9) 안티 패턴(피해야 할 것)
UIBarButtonItem에 음수 인셋을 과하게 주어 아이콘을 바 바깥으로 밀어내기contentEdgeInsets와imageEdgeInsets를 큰 값으로 동시에 변경해 탭 영역이 44pt 미만으로 줄어드는 것- iOS 15+에서
UIButton.Configuration과 EdgeInsets 혼용 (예측 어려움) - RTL 무시: 좌우 인셋을 하드코딩하고 미러링을 고려하지 않음
- 아이콘마다 임의 크기 혼용(예: 15pt/21pt 섞기) → 같은 줄에서 들쑥날쑥
10) 유지보수 전략
- 심볼별 기본 인셋 테이블을 딕셔너리로 유지 (섹션 4.3)
- 디바이스 Size Class별(Compact/Regular)로 보정값 분기
- 다크/라이트 전환 체크(특히 Template vs Original 렌더링)
- UI 테스트 스냅샷으로 레그레션 방지(인셋이 바뀌면 실패하도록)
결론
imageInsets는 미세 조정 도구입니다.- 탭 영역(44×44)·시각적 균형·일관성을 잃지 않는 선에서, 글리프마다 소량의 보정만 하세요.
UIBarButtonItem을 쓰되 내부는 커스텀 UIButton으로 다루는 패턴이 안정적이며,
RTL·LargeTitle·iPad까지 고려한 도우미 레이어(팩토리/확장)를 만들어 두면 팀 전체 품질과 속도가 올라갑니다.
필요하면 지금 프로젝트에 맞춘 심볼별 인셋 테이블 초안을 바로 생성해서 적용까지 정리해 줄게요.
답글 남기기