• Background

    • 관련 주제로 Dynamic Type 이 있다.
      • 시스템의 텍스트 크기를 커스텀할 수 있는 기능이다.
      • 기본적으로는 7가지만 지원하지만, 접근성 옵션을 키면 추가적으로 5단계를 제공한다.
    • Dynamic type을 설정해도, 화면의 일부 텍스트는 따라 커지지 않는다.
      • 탭바의 글씨라던지(그 위에 아이콘도 그렇고)
      • 근데 이걸 키우면 컨텐츠 영역이 너무 줄어든다.
    • 그래서 이 경우는 길게 누르면 큰 버젼으로 볼 수 있는 기능을 제공한다.
      • 롱프레스 상태에서 드래그하면서 버튼을 찾고, 탭하기를 원하는 곳에서 멈추면 탭한 것 처럼 동작한다.
    • 이 기능을 Large Content Viewer라고 한다.
      • 저시력 사용자가 탭바나 다른 작게 유지되어야만 하는 UI를 사용하는 것을 가능하게 해준다.
    • Large Content Viewer는 적절한 때에 사용되어야만 한다.
      • 접근성 텍스트 크기로 설정했을 때만 활성화된다.
    • 지원하는 뷰
      • 표준 UIKit의 bar
        • Navigation bar
        • Tab bar
        • Toolbar
        • Status bar
      • 커스텀 UI의 경우도 관련 API를 사용해서 표준 UIKit 컨트롤처럼 지원할 수 있다.
        • 이 기능은 Custom UI가 Dynamic Type 에 맞춰서 커질 수 없는 경우에만 쓰는 기능이다.
        • 가능하면 Large Content Viewer보다는 Dynamic Type을 지원하는 게 좋다.
  • API

    • 표준 UIKit 컨트롤
      • 기본적으로 지원하긴 하는데, 잘 보이려면 몇가지를 고려해야 한다.

        • 아이콘으로 PDF 이미지를 쓰고 있다면 Preserve Vector Data 옵션에 체크

        • 레스터라이징 된 이미지라면, 큰 버전을 API로 제공하는 방법이 있다.

          // UIBarItem
          var largeContentSizeImage: UIImage?
          
          open var largeContentSizeImageInsents: UIEdgeInsets
          
      • 커스텀 UI

        • iOS 13 이전에는 기본적으로 알아서 처음부터 만들어야 했다.

        • 13부터 표준 UIKit과 동일한 형태로 지원할 수 있는 API를 제공한다.

          • 어떤 버튼을 Content Viewer를 통해서 보여줄 지를 명시해야 한다.
          • 각 뷰마다 보여줄 타이틀이나 이미지, 혹은 둘 다를 제공해야 한다.
            • 버튼 구현에 표준 UIKit 요소를 사용하면 기본으로 해준다.
          • 커스텀 탭바 자체에 제스처 설정도 해줘야 한다.
        • UILargeContentViewerItem 프로토콜

          public protocol UILargeContentViewerItem: NSObjectProtocol {
          	// Large Content Viewer를 통해서 해당 뷰를 보여줄 지 여부
          	var showsLargeContentViewer: Bool { get }
          
            var largeContentTitle: String? { get }
          
            var largeContentImage: UIImage? { get } 
          
          	// 원래의 작은 이미지를 스케일해서 사용할 것인지 여부
          	// PDF + Preserve Vector Data 일때 true로 해놓으면 됨.
            var scalesLargeContentImage: Bool { get } 
          
          	var largeContentImageInsets: UIEdgeInsets { get }
          }
          
        • UIView가 위 프로토콜을 구현하고, setter를 제공하기 떄문에 서브클래싱할 필요 없이 설정할 수 있다.

          extension UIView {
          	var showsLargeContentViewer: Bool
          
            var largeContentTitle: String?
          
            var largeContentImage: UIImage?
          
            var scalesLargeContentImage: Bool
          
          	var largeContentImageInsets: UIEdgeInsets
          }
          
        • UIButton, UILabel등은 타이틀과 이미지의 기본값을 제공하기도 한다.

        • 탭뷰 자체에 인터렉션을 추가해야 하는데, 이는 UIInteraction을 추가함으로써 이뤄진다.

          • drag N drop을 지원할 때 쓰던 것이다.
          extension UIView {
          	func addInteraction(_ interaction: UIInteraction)
          }
          
        • Large Content Viewer를 쓰기위한 Interaction

          • 간단한 경우는 인자없이 그냥 만들고 프로퍼티 건드릴 것도 없다.
            • 다른 gestureRecognizer가 들어가 있는 경우는 커스텀이 필요하다.
          • delegate를 제공해서 섬세한 컨트롤이 가능하다.
          class UILargeContentViewerInteraction: NSObject, UIInteraction {
          	init(delegate: UILargeContentViewerInteractionDelegate?)
          
          	weak var delegate: UILargeContentViewerInteractionDelegate? { get }
          
            var gestureRecognizerForExclusionRelationship: UIGestureRecognizer { get }
          
            class var isEnabled: Bool { get }
          }
          
          extension UILargeContentViewerInteraction { 
          	class let enabledStatusDidChangeNotification: NSNotification.Name
          }
          
          protocol UILargeContentViewerInteractionDelegate: NSObjectProtocol {
            // 사용자가 마지막으로 손을 뗐을 때 호출
            // 이를 구현하지 않고, 표준 UIKit 컨트롤을 쓰면 touchUpInside 이벤트가 날아간다.
          	optional func largeContentViewerInteraction(_ interaction: UILargeContentViewerInteraction,
          								didEndOn item: UILargeContentViewerItem?,
          								at point: CGPoint)
          
          	// 특정 포인트에 대한 아이템을 반환
            // 제공하지 않을 경우 뷰 계층을 재귀적으로 뒤져서 찾아냄
            // 뷰를 사용하지 않는 경우에서 유용
           	optional func largeContentViewerInteraction(_ interaction: UILargeContentViewerInteraction,
          								itemAt point: CGPoint) -> UILargeContentViewerItem?
          
            // Large Content Viewer를 담을 ViewController
            // 기본적으로 해당 인터렉션이 추가된 뷰를 가지고 있는 뷰컨트롤러를 반환
          	optional func viewController(for interaction: UILargeContentViewerInteraction) -> UIViewContorller
          
          }
          
  • Examples

    스크린샷 2023-03-09 오후 10.46.26.png

    button.showsLargeContentViewer = true
    label.showsLargeContentViewer = true
    customBar.addInteraction(UILargeContentViewerInteraction())
    

    스크린샷 2023-03-09 오후 10.47.46.png

    class MyCustomButton: UIView {
    	override var showsLargeContentViewer: Bool {
    		get {
    			return true
    		}
    
    		set { }
    	}
      
      override var largeContentTitle: String? {
    		get { 
    			return myTitle
    		}
    
    		set { }
    	}
    
    	 override var largetContentImage: UIImage? {
    		get { 
    			return mySmallImage
    		}
    
    		set { }
    	}
    
    	// pdf 이미지를 쓴다고 가정
    	override var scalesLargeContentImage: Bool {
    		get { 
    			return true
    		}
    
    		set { }
    	}
    }
    
  • longPressGesture를 이미 쓰고 있는 경우 겹치지 않게 하기

    longPressRecognizer.minimumPressDuration = UILargeContentViewerInteraction.isEnabled ? 3.0 : 1.0
    
    NotificationCenter.default.addObserver(forName: UILargeContentViewerInteraction.enabledStatusDisChangeNotification, object: nil, queue: nil) { _ in
    	longPressRecognizer.minimumPressDuration = UILargeContentViewerInteraction.isEnabled ? 3.0 : 1.0
    }
    
    longPressRecognizer.delegate = self
    
    // delegate
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
    	shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    	
    	return gestureRecognizer == longPressRecognizer && otherGestureRecognizer == largeContentViewerInteraction.gestureRecognizerForExclusiveRelationShip
    }