[카테고리:] 미분류

  • 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까지 고려한 도우미 레이어(팩토리/확장)를 만들어 두면 팀 전체 품질과 속도가 올라갑니다.

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