iOS 내비게이션 바 아이콘

원리→철학→실전 레시피→디버깅 순서로 정리한 종합 가이드다.


1) 인셋 3총사: 개념 정리

UIKit에서 인셋은 3종이 있다. 각각의 영향 범위를 혼동하면 의도와 다른 결과가 나온다.

  • UIButton.contentEdgeInsets
    버튼 전체 콘텐츠 박스(= 이미지+타이틀) 바깥 여백. 탭 영역과도 연동될 수 있어 과대/과소 설정 주의.
  • UIButton.imageEdgeInsets
    콘텐츠 박스 내부에서 이미지(아이콘) 위치만 이동.
  • UIButton.titleEdgeInsets
    콘텐츠 박스 내부에서 타이틀 위치만 이동.

부호 규칙: left양수를 주면 실제 콘텐츠는 오른쪽으로 밀린다. top 양수는 아래로 이동.


2) 왜 UIBarButtonItem.imageInsets가 안 보일까?

순정 UIBarButtonItem에는 imageInsets 프로퍼티가 없다. 많은 코드베이스는 다음 둘 중 하나를 사용한다.

  1. 커스텀 UIButtoncustomView로 넣는 패턴
    이 경우 인셋 조절은 버튼 인스턴스의 imageEdgeInsets/contentEdgeInsets에 해야 한다.
  2. 확장(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) “아이콘이 왜 안 바뀌지?” 원인과 해결

자주 보는 원인

  1. 바 버튼이 customView(UIButton) 기반인데 UIBarButtonItem.image만 바꿈
    item.customView as? UIButtonsetImage 해야 반영.
  2. 다른 코드가 나중에 덮어씀
    viewWillAppear/fetch()/델리게이트 콜백 뒤에 재적용.
  3. 표시 순서 착시
    rightBarButtonItems의 첫 요소가 오른쪽 끝. 배열 순서를 재확인.
  4. 구성 충돌
    → iOS 15+에서 UIButton.Configuration와 인셋을 혼용하면 예측이 어려움. 한 축만 사용.
  5. 렌더링 모드/틴트
    → 틴트색 대비가 약하면 바뀌지 않은 것처럼 보인다. 필요 시 .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·대체 심볼을 함께 고려한다.
  • 덮어쓰기/구성 충돌/배열 순서 착시가 흔한 함정이며, 로그·스냅샷 테스트·버전 폴백으로 안정성을 높인다.

코멘트

답글 남기기

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