이번에 앱 만드는 과정에서 다크모드를 설정하는 기능을 구현하고자 했습니다.

 

다크모드는 iOS13부터 적용되어 각각 사용자의 화면모드에 맞춰서 다크모드를 설정할 수 도있지만,

사용자의 모드와 상관없이 앱의 다크모드를 설정하려면 추가옵션이 필요하다고 느꼈습니다.

다크모드 옵션

  • 사용자 지정(사용자의 화면모드에 맞춰짐)
  • 다크모드
  • 라이트모드

이 세 가지를 구현하고자 합니다

 

이전 포스팅에서 봤을지 모르지만 UserDefaults를 이용하여 매번 뷰컨트롤에서 다크모드 값을 확인하고 그리는 방식을 사용했었습니다.

 

 

다크모드 값을 유저디폴트로 저장하고 settingUI 함수를 매번 뷰 그릴 때마다 호출하곤 했습니다.

이는 뷰컨이 많아지고, 여러 사람들과 함께 작업을 한다면, 불필요한 코드임을 느끼고 다른 방법을 사용하고자 합니다.

위 방법이 정답이 될 수 있지만, 여러 가지를 시도해 보고자 이번에는 ColorAsset을 사용하여 코드를 줄이고자 합니다.

 

 

먼저 에셋에 색 옵션을 넣습니다.

iOS에서 UI객체 중 일부는 색을 지정하지 않으면 사용자의 모드에 맞춰지는 시스템컬러가 디폴트값인 경우가 있습니다.

레이블 같은 경우 따로 색을 넣지 않아도 다크모드에 대응하게 됩니다. 하지만 예기치 못한 곳에서 의도와 다르게 UI가 그려질 수 있으므로

모든 경우의 색 조합을 넣는 게 좋습니다. 저는 아래와 같이 세 가지를 넣었습니다.

 

1. 컬렉션뷰나 테이블뷰의 셀 배경색을 지정할 색: CellBackgroundColor

2. Label 및 버튼 색을 지정할 색: LabelColor

3. 뷰의 배경색을 지정할 색: ViewBackgroundColor

 

구현한 세 가지 색을 각 인스턴스에 지정하는 방법은 아래와 같습니다.

// 컬렉션뷰, 테이블뷰의 배경색 지정
collectionView.backgroundColor = UIColor(named: "ViewBackgroundColor")
tableView.backgroundColor = UIColor(named: "ViewBackgroundColor")

// 레이블과 버튼 색 지정
label.textColor = UIColor(named: "LabelColor")
button.setTitleColor(UIColor(named: "LabelColor"), for: .normal)

// 셀의 배경색 지정
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as? CollectionViewCell else { return UICollectionViewCell() }
    cell.backgroundColor = UIColor(named: "CellBackgroundColor")
    return cell
}

 

이렇게 각 UI인스턴스에 지정해 준다면, 다크모드 혹은 라이트모드, 사용자 지정모드에 맞춰서 설정된 색으로 그려집니다.

 

그렇다면 현재 사용자는 어떤 모드를 설정했는지 어떻게 알 수 있을까요?

다양한 방법이 있겠지만 저는 UserDefaults를 이용하여 내부 DB에 값을 저장했습니다.

(내부 DB는 앱을 삭제하면 모두 날아가므로 안전성이 보장된 방법이 아닙니다!)

 

다크모드에 대한 UserDefaults 싱글톤 구현

// UIModeUserDefaults 싱글톤
class UIModeUserDefaults {
    static let shared = UIModeUserDefaults()
    init() {}
    // 저장된 값으로 초기화
    // 한번도 값이 설정되지 않았다면 nil
    var stringValue = UserDefaults.standard.string(forKey: "mode")
    
    // 저장된 값을 UIMode 형식으로 저장
    // 한번도 값이 설정되지 않았다면 default를 통해 .custom으로 지정(사용자지정모드)
    var modeValue: UIMode {
        switch self.stringValue {
        case "custom":
            return .custom
        case "dark":
            return .dark
        case "light":
            return .light
        default:
                return .custom
        }
    }
    // 값 변경후 value 갱신
    func chageValue(_ mode: UIMode) {
        switch mode {
        case .dark:
            UserDefaults.standard.setValue("dark", forKey: "mode")
        case .light:
            UserDefaults.standard.setValue("light", forKey: "mode")
        case .custom:
            UserDefaults.standard.setValue("custom", forKey: "mode")
        }
        stringValue = UserDefaults.standard.string(forKey: "mode")
    }
}

 

 

왜 싱글톤으로 구현했는가?

1. 어디서든 인스턴스 생성 없이 쉽게 접근이 가능하도록 하기 위해

2. 접근도 되면서 값수정도 쉽게 가능하다.

 

이제 값을 저장할 객체도 만들었으니, 값을 수정할 액션을 만들고자 합니다.

예제는 가볍게 버튼 세 가지로 구현했습니다.

  • Custom Button: 사용자 지정으로 설정
  • Dark Button: 다크모드로 설정
  • Light Button: 라이트모드로 설정

아래는 정상적으로 모드가 활성화되는지 확인하기 위해 임의의 테이블뷰, 컬렉션뷰를 넣은 모습입니다.

 

<버튼 액션 함수>

//    MARK: Button Actions
	// 사용자지정 버튼 액션
    @IBAction func CustomButtonTapped(_ sender: Any) {
    	// 값 변경
        UIModeUserDefaults.shared.chageValue(.custom)
        // 사용자 지정 모드로 변경
        view.window?.overrideUserInterfaceStyle = .unspecified
    }
    // 다크모드 버튼 액션
    @IBAction func DarkButtonTapped(_ sender: Any) {
        // 값 변경
        UIModeUserDefaults.shared.chageValue(.dark)
        // 다크모드로 변경
        view.window?.overrideUserInterfaceStyle = .dark
    }
    // 라이트모드 액션
    @IBAction func LightButtonTapped(_ sender: Any) {
        // 값 변경
        UIModeUserDefaults.shared.chageValue(.light)
        // 라이트모드로 변경
        view.window?.overrideUserInterfaceStyle = .light
    }

 

overrideUserInterfaceStyle

https://developer.apple.com/documentation/uikit/uiviewcontroller/3238087-overrideuserinterfacestyle

 

overrideUserInterfaceStyle | Apple Developer Documentation

The user interface style adopted by the view controller and all of its children.

developer.apple.com

overrideUserInterfaceStyle는 뷰컨트롤과 모든 하위항목이 해당 사용자인터페이스 스타일로 채택됩니다.

이 속성은 강제성이 있기 때문에 값이 설정되면 모든 뷰 계층의 항목들이 해당 값으로 변경됩니다.

값은 세 가지가 있습니다. (dark, light, unspecified)

 

<결과 화면>

 

버튼을 누르면 그에 맞춰서 모드가 바뀌고 있습니다.

 

마지막 단계로 앱을 실행했을 때, 저장된 모드에 맞춰서 설정하는 방법입니다.

현재 값 불러오기 및 수정은 구현되어 있지만, 앱이 실행 됐을 때의 해당 모드로의 설정이 구현되어 있지 않습니다.



이전에 UI인스턴스에 색을 지정할 때 UIColor(named:)로 지정했기에 값만 변경된다면 그에 맞춰서 색이 바뀌게끔 구현되어 있습니다.

또한 overrideUserInterfaceStyle로 뷰 계층과 모든 하위항목이 값에 맞춰서 색이 변경되기 때문에

앱을 실행할 때 딱 한번 지정해 주면 됩니다.

// 앱이 실행될때 메서드가 실행된다.
// 새로운 UIWindow를 만들고 루트뷰컨트롤러가 설정됨
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        
        // 저장된 값에 따른 overrideUserInterfaceStyle 설정
        switch UIModeUserDefaults.shared.modeValue {
        case .dark:
            window?.overrideUserInterfaceStyle = .dark
        case .light:
            window?.overrideUserInterfaceStyle = .light
        case .custom:
            window?.overrideUserInterfaceStyle = .unspecified
        }
}

 

이렇게 ColorAssets을 이용한 다크모드 설정을 보았습니다.

 

개인적으로 느낀 점은 코드가 이전보다 간결해짐을 느낍니다.

viewDidLoad와 viewWillAppear에서 모드에 맞춰서 UI 그리는 함수도 호출하지 않아도 되고,

이곳저곳에서 색변경하는 동작을 하지 않아도 됩니다.

 

이 방법이 꼭 정답만은 아닙니다.

다만 여러 가지 방법을 알고 있으면 어떤 상황에서 기능을 구현할 때 선택지가 다양해지고,

효율적으로 구현할 수 있을 거라 기대하면서 이 글을 마칩니다.

 

프로젝트 전체코드:

https://github.com/minjae-L/UIDarkModeTest

 

GitHub - minjae-L/UIDarkModeTest: UIDarkMode 설정 예시 프로젝트입니다.

UIDarkMode 설정 예시 프로젝트입니다. Contribute to minjae-L/UIDarkModeTest development by creating an account on GitHub.

github.com

 

 

 

+ Recent posts