LINE Free Call에 iOS 다크 모드 적용하기

iOS 다크 모드

macOS의 다크 모드(dark mode)가 공개된 지 1년 만인 2019년 6월, WWDC에서 iOS를 위한 다크 모드가 공개되었습니다. 다크 모드란 iOS 13부터 지원되는 기능으로 사용자가 다크 모드를 선택하면 배경색을 어둡게 하고 텍스트, 아이콘 등의 인터페이스 요소를 밝게 변경해 줍니다. 기존 흰 배경색에 검은 글씨의 화면 스타일은 라이트 모드(light mode)라고 부르게 되었습니다.

다크 모드는 라이트 모드에 비해 저조도 환경에 이상적이고, 어두운 곳에서도 화면의 내용을 쉽게 읽을 수 있게 도와줍니다. 또한, 화면을 밝게 하는데 필요한 전력이 줄어들기 때문에 배터리 수명에 도움이 될 수도 있습니다.

사용자가 다크 모드를 선택하면 Apple에서 제공하는 기본 앱은 자동으로 색상이 변경되지만, 서드 파티 앱에서는 다크 모드를 지원하기 위해 추가적인 작업을 해주어야만 합니다. 이번 글에서는 LINE Free Call에서 다크 모드를 지원하기 위해 했던 작업과 소소한 팁에 대해 소개해드리고자 합니다.

 

색상과 이미지

다크 모드를 적용하기에 앞서 가장 먼저 해야 할 일은 다크 모드로 설정을 변경한 뒤 화면이 어떻게 보이는지 파악하는 일입니다. iOS에서 기본으로 제공하는 뷰나 컨트롤을 별다른 수정 없이 사용하고 있었다면 iOS가 자동으로 색상을 변경해 주기 때문에 생각보다 많은 작업이 필요하지 않을 수도 있습니다.

가장 먼저 살펴볼 부분은 색상입니다. 기존에는 한 가지의 색상만 설정해도 화면을 구현하기에 충분했기 때문에 대체로 하나의 고정된 컬러 값을 이용하여 색상을 설정하였습니다. 하지만 다크 모드가 추가되면서 같은 뷰나 컨트롤일지라도 화면 스타일에 따라 다른 색상을 사용할 수 있게 되었습니다. 이런 복수의 색상을 관리하기 위해 Xcode 11부터 UIColor.systemRedUIColor.labelColorUIColor.systemBackgroundColor와 같이 모드에 따라 적절한 색상이 미리 설정된 Semantic Colors를 제공하고 있습니다.

하지만 LINE은 시스템에서 기본적으로 제공해 주는 Semantic Colors 이외의 색상을 사용하는 경우가 많아 asset catalog에 Custom Color Set을 등록하여 색상 팔레트를 관리하도록 수정하였습니다. Asset catalog에 Color Set를 추가하면 Interface Builder의 Attributes Inspector를 통해 추가된 Color Set의 이름을 선택하거나, 아래와 같은 코드를 이용해 색상을 설정할 수 있습니다. 또한 화면 스타일이 변경될 때마다 iOS에서 자동으로 맞는 색상을 설정해 주니 매우 편리하죠! 참고로 Any Appearance는 다크 모드를 지원하지 않는 이전 버전에서 표시될 색상을 뜻합니다.

// Semantic color
if #available(iOS 13, *) {
    backgroundColor = .systemBackground
    tintColor = .systemGreen
}
 
// Custom color in asset catalog
tintColor = UIColor(named: "customColorName")

일반적인 경우에 이미지도 asset catalog를 이용하여 각 화면 스타일에서 사용할 이미지를 추가해주는 것만으로도 라이트 모드와 다크 모드를 지원할 수 있습니다. 하지만 LINE처럼 이미 수많은 이미지를 사용하고 있는 앱에서는, 다크 모드를 지원하기 위해 단순히 이미지를 추가해 버린다면 추가한 이미지만큼 앱 용량이 늘어나는 부담이 발생합니다. 따라서 이번 작업에서는 이미지 파일을 최소한으로 추가하며 다크 모드를 지원할 수 있는 방법을 찾는데 초점을 두었습니다.

 

UIImageView와 UIButton

LINE Free Call에서 사용하고 있는 대부분의 이미지들은 UIImageView나 UIButton에서 사용하는 단색의 아이콘 이미지입니다. 또한 각각의 상태(state)를 다른 색상으로 구분하여 표시하기 위해 필요한 상태의 개수만큼 동일한 모양의 다른 색상의 이미지를 여러 장 사용하고 있었습니다. 따라서 이미지와 버튼을 추가하거나 아이콘 이미지가 변경될 때마다 상태에 맞게 아이콘 이미지를 설정해야 하는 불편함이 있었습니다. 이런 불편함을 해소하기 위해 한 장의 아이콘 이미지와 tintColor를 이용하여 각 상태에 맞는 색상의 아이콘을 표시할 수 있는 SemanticImageView와 SemanticButton 클래스를 만들기로 했습니다.

먼저 SemanticImageView 클래스부터 살펴보겠습니다. 아이콘 이미지에 tintColor를 적용하기 위해서는 이미지의 렌더링 모드(RenderingMode)를 template로 변경해야 합니다(기본 렌더링 모드는 automatic입니다). 이미지를 template 모드로 렌더링하게 되면 원래 이미지가 가지고 있던 색상 정보를 무시하고, 이미지의 형태만을 사용할 수 있게 됩니다. iOS 13부터는 렌더링 모드와 tintColor를 한 번에 변경할 수 있는 새로운 메서드도 제공하고 있지만, 2019년 현재 LINE은 iOS 11부터 지원하고 있기 때문에 iOS 하위 버전에서도 사용할 수 있는 메서드를 사용하였습니다. Asset catalog에서 사용할 이미지를 선택하고 오른쪽 Attribute Inspector에서 Render As 항목을 Template Image로 설정해도 무방합니다.

/*
    @available(iOS 7.0, *)
    open func withRenderingMode(_ renderingMode: UIImage.RenderingMode) -> UIImage
 
    @available(iOS 13.0, *)
    open func withTintColor(_ color: UIColor, renderingMode: UIImage.RenderingMode) -> UIImage
*/
 
class SemanticImageView: UIImageView {
    override func awakeFromNib() {
        super.awakeFromNib()
 
        // set image and highlightedImage again with template rendering mode
        image = image?.withRenderingMode(.alwaysTemplate)
        highlightedImage = highlightedImage?.withRenderingMode(.alwaysTemplate)
    }
 
    ...
}

LINE Free Call은 UI 작업할 때 ‘Interface Builder’를 사용하고 있습니다. @IBInspectable을 이용하여 Interface Builder에서 상태별 tintColor을 손쉽게 설정할 수 있도록 프로퍼티를 선언합니다. @IBInspectable은 프로퍼티 값을 Interface Builder의 Attribute Inspector에서 설정할 수 있도록 도와줍니다.

class SemanticImageView: UIImageView {
    @IBInspectable var normalTintColor: UIColor
    ...
}

마지막으로 SemanticImageView의 상태가 변하는 시점에 알맞는 tintColor로 변경해주기만 하면 완성입니다. UIImageView는 highlighted 속성이 변경될 수 있기 때문에 isHighlighted를 오버라이드하여 이벤트를 처리합니다.

class SemanticImageView: UIImageView {
    ...
 
    override var isHighlighted: Bool {
        didSet { tintColor = isHighlighted ? highlightedTintColor : normalTintColor }
    }
}

서브 클래스로 만든 SemanticImageView 클래스로 구현한 테스트 영상입니다. 화면 스타일과 highlighted 속성에 따라 다른 tintColor가 설정되는 것을 확인할 수 있습니다.

버튼도 highlighted 이외에 selected, disabled 상태가 추가된다는 것을 신경 쓴다면 크게 다르지 않은 구조로 서브 클래스를 만들 수 있습니다. 아래 예제는 highlighted와 selected 상태를 추가로 고려한 케이스입니다. isHighlighted와 isSelected를 오버라이드하고, 상태가 변경되면 updateTintColor() 메서드를 통하여 상태에 맞게 tintColor을 변경해줍니다. 또한 UIButton의 상태는 [.selected, .highlighted]와 같이 여러 상태를 동시에 가질 수 있는 OptionSet이기 때문에 필요한 상태를 고려하며 구현하여야 하는 점에 주의합니다.

class SemanticButton: UIButton {
    @IBInspectable var normalTintColor: UIColor
    @IBInspectable var highlightedTintColor: UIColor
    @IBInspectable var selectedTintColor: UIColor
    @IBInspectable var selectedHighlightedTintColor: UIColor
    ...
 
    override func awakeFromNib() {
        super.awakeFromNib()
 
        // set image again with template rendering mode
        setImage(image(for: .normal)?.withRenderingMode(.alwaysTemplate), for: .normal)
        ...
    }
 
    override var isHighlighted: Bool {
        didSet { updateTintColor() }
    }
 
    override var isSelected: Bool {
        didSet { updateTintColor() }
    }
 
    func updateTintColor() {
        switch state {
        case [.selected, .highlighted]: tintColor = selectedHighlightedTintColor
        case .highlighted: tintColor = highlightedTintColor
        case .selected: tintColor = selectedTintColor
        default: tintColor = normalTintColor
        }
    }
 
    ...
}

검은색 원 이미지를 가진 버튼을 누를 때마다 바뀌는 SemanticButton의 상태와 화면 스타일에 따라 버튼의 tintColor가 변경되는 테스트 영상입니다.

 

그래서 무엇이 좋아졌나요?

다크 모드에 대응하면서 이미지를 최대한 적게 추가하기 위해 시작한 작업이었는데요. tintColor를 이용한 덕분에 이전 버전보다 사용하고 있는 이미지 리소스 개수를 오히려 더 줄일 수 있었습니다. 게다가 아이콘의 색상이 변경될 때마다 매번 디자이너에게 수정된 이미지를 전달받아 적용하는 과정을 거쳐야 했었는데 이젠 컬러 값만 전달받아 이미지의 tintColor를 변경하는 것으로 간소화할 수 있어 작업의 효율도 조금 더 올랐습니다. 화면 스타일별로 이미지를 추가하는 방법이 가장 쉽고 빠르고 간편하긴 하지만, 혹시라도 개발하는 앱의 크기를 관리해야 하거나 다크 모드에 대응하기 위해 추가해야 할 이미지가 많을 경우에 고려해 볼 만한 방법이라고 생각합니다.

다크 모드가 적용된 LINE Free Call은 곧 공개될 예정입니다. 기대해주세요!

Related Post