iOS 내비게이션 아이콘 .imageInsets

(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 프로퍼티가 없습니다.
  • 많은 코드베이스가 커스텀 UIButtoncustomView로 래핑하거나,
    **확장(Extension)으로 imageInsets**을 추가해 내부 버튼의 imageEdgeInsets에 위임합니다.
  • 따라서 다음 둘 중 하나로 동작합니다:
    1. UIBarButtonItem.customView ← (UIButton) ← imageEdgeInsets 조절
    2. 팀 공용 확장: barButton.imageInsets = ... → 내부적으로 customView의 imageEdgeInsets로 전달

현재 프로젝트처럼 barButton.imageInsets = ...컴파일된다면, 내부에 이런 확장이 들어있다고 봐도 됩니다.


2) 왜 인셋이 필요한가? — UI 철학

2.1 “수학적 중앙”과 “시각적 중앙”의 차이

  • SF Symbols는 글리프마다 여백·무게 중심이 다릅니다.
    예: qrcode(정사각형) vs qrcode.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}
      → 오른쪽 아이콘을 약간 안쪽으로 끌어당겨 좌우 간격시각적 그룹 형성
  • 좌측 아이콘(앱 아이콘·블록키)
    • 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.ConfigurationcontentInsets, imagePadding 등을 제공합니다.
  • imageEdgeInsets동시에 쓰면 예측이 어려워질 수 있으므로,
    하나의 축(Configuration 기반 또는 EdgeInsets 기반)만 택하는 걸 권장합니다.
    내비바 커스텀뷰에서는 EdgeInsets 기반이 관리가 쉽습니다.

5.4 alignmentRectInsets(이미지 자체의 정렬 사각형)

  • 일부 커스텀 이미지(PDF)는 정렬 사각형이 비대칭일 수 있습니다.
  • 이 경우 버튼 인셋이 아니라 이미지 에셋 자체를 손보는 편이 낫습니다.
    (여백 통일, 아트보드 좌표 정리)

5.5 대각선 스냅(픽셀 힌팅)

  • 1pt 단위 인셋이 레티나에서 반픽셀 경계를 타면 흐릿해 보일 수 있습니다.
  • 수치 조정 시 짝수 pt 위주로 미세 조절해 보는 것도 방법입니다.

6) 접근성·제스처·안전성

6.1 탭 영역 보장

  • customViewframe을 명시적으로 44×44pt 이상으로 잡으세요.
  • 인셋 때문에 보이는 이미지가 작아져도 상관없지만, 터치 타겟은 줄지 않게.

6.2 VoiceOver 라벨

barButton.accessibilityLabel = "My QR Code"
barButton.accessibilityTraits = .button

6.3 UIGestureRecognizer와의 상호작용

  • customView 위에 제스처를 추가하면 터치 전달 순서가 복잡해질 수 있습니다.
  • 내비바 아이콘은 버튼 하나로 단순 유지하는 게 안전합니다.

7) 디버깅·튜닝 체크리스트

  1. 런타임 뷰 계층 확인 print(navigationController.view.debugDescription)
  2. 실제 이미지 렌더 크기 확인 let cfg = UIImage.SymbolConfiguration(pointSize: 17, weight: .regular) let img = UIImage(systemName: "qrcode", withConfiguration: cfg) print(img?.size ?? .zero)
  3. RTL 시뮬레이션
    iOS 설정 → 언어·지역 → 아랍어/히브리어 추가 → 우선순위 올려 테스트
  4. Large/Inline/Compact 상태 전환
    스크롤로 LargeTitle 토글, 회전(세로/가로), iPad 멀티윈도우
  5. 손가락 테스트
    엄지로 빠르게 탭해도 잘 눌리는지(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음수 인셋을 과하게 주어 아이콘을 바 바깥으로 밀어내기
  • contentEdgeInsetsimageEdgeInsets큰 값으로 동시에 변경해 탭 영역이 44pt 미만으로 줄어드는 것
  • iOS 15+에서 UIButton.ConfigurationEdgeInsets 혼용 (예측 어려움)
  • RTL 무시: 좌우 인셋을 하드코딩하고 미러링을 고려하지 않음
  • 아이콘마다 임의 크기 혼용(예: 15pt/21pt 섞기) → 같은 줄에서 들쑥날쑥

10) 유지보수 전략

  • 심볼별 기본 인셋 테이블을 딕셔너리로 유지 (섹션 4.3)
  • 디바이스 Size Class별(Compact/Regular)로 보정값 분기
  • 다크/라이트 전환 체크(특히 Template vs Original 렌더링)
  • UI 테스트 스냅샷으로 레그레션 방지(인셋이 바뀌면 실패하도록)

결론

  • imageInsets미세 조정 도구입니다.
  • 탭 영역(44×44)·시각적 균형·일관성을 잃지 않는 선에서, 글리프마다 소량의 보정만 하세요.
  • UIBarButtonItem을 쓰되 내부는 커스텀 UIButton으로 다루는 패턴이 안정적이며,
    RTL·LargeTitle·iPad까지 고려한 도우미 레이어(팩토리/확장)를 만들어 두면 팀 전체 품질과 속도가 올라갑니다.

필요하면 지금 프로젝트에 맞춘 심볼별 인셋 테이블 초안을 바로 생성해서 적용까지 정리해 줄게요.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다