[카테고리:] 미분류

  • 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·대체 심볼을 함께 고려한다.
    • 덮어쓰기/구성 충돌/배열 순서 착시가 흔한 함정이며, 로그·스냅샷 테스트·버전 폴백으로 안정성을 높인다.