날씨앱 프로젝트 - 2일차

maxminseok·2025년 1월 8일

오늘은 이런 뷰를 만들면서, 라이트모드와 다크모드를 적용시키는 작업을 진행했다.

// 라이트모드/다크모드 적용
private func updateMode(_ mode: ThemeMode) {
    switch mode {
    case .light:
        UIApplication.shared.windows.forEach { window in
            window.overrideUserInterfaceStyle = .light
        }
        updateCheckMaker(lightModeButton)
    case .dark:
        UIApplication.shared.windows.forEach { window in
            window.overrideUserInterfaceStyle = .dark
        }
        updateCheckMaker(darkModeButton)
    }
}

라이트모드와 다크모드 적용을 하기 위해 ViewController에 이런 메서드를 작성했는데,

'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead 이런 경고가 떴다.

무슨 말이냐 하면,
iOS 15 버전부터 멀티 윈도우 지원이 확장되면서 특정 UIWindowScene에 속한 윈도우를 명시적으로 다루는 것이 중요해졌기 때문에, UIApplication.shared.windows는 iOS 15부터 사용이 권장되지 않는다고 한다.

UIApplication.shared.windows는 앱의 UI 계층을 직접 탐색하고 제어하는 데 사용되었던 방식으로, 앱에서 현재 활성화된 모든 UIWindow 객체를 배열로 반환하는 속성이다.

과거 iOS에는 Scene 개념이 없었기 때문에, 앱 내의 모든 윈도우를 단순히 가져오는 방식으로 UI를 제어할 수 있었다고 한다.

// 라이트모드/다크모드 적용
private func updateMode(_ mode: ThemeMode) {
    switch mode {
    case .light:
        // iOS 15 이상에서 권장되는 방식
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .forEach { windowScene in
                windowScene.windows.forEach { window in
                    window.overrideUserInterfaceStyle = .light
                }
            }
        updateCheckMaker(lightModeButton)
        
    case .dark:
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .forEach { windowScene in
                windowScene.windows.forEach { window in
                    window.overrideUserInterfaceStyle = .dark
                }
            }
        updateCheckMaker(darkModeButton)
    }
}

따라서 코드를 위와 같이 수정하였다.

주요 변경점과 내용

UIApplication.shared.connectedScenes 사용

  • 앱에서 활성화된 모든 장면(Scene)을 가져온다.
    • Scene이란 iOS 13 버전 부터 도입된 것으로, Scene은 앱의 UI와 상태를 독립적으로 관리하는 단위이다.
    • 예를 들어 iPad에서 멀티 윈도우 기능을 사용할 때 각각의 창(Window)은 개별 Scene에 속한다.
    • CarPlay나 외부 디스플레이에서 앱이 실행될 때도 별도의 Scene이 생성된다.
  • 이전에는 UIApplication.shared.windows를 사용해 앱의 모든 윈도우를 가져왔지만, iOS 15 이후에는 활성화된 특정 Scene(UIWindowScene)을 명시적으로 다루는 방식이 요구된다

compactMap { $0 as? UIWindowScene }

  • 활성화된 장면 중 UIWindowScene 객체만 추출한다.
    • connectedScenesUIScene 타입의 집합(Set)이기 때문에 UIWindowScene(윈도우와 연결된 Scene)을 추출하는 것이다.
    • 따라서 compactMap으로 connectedScenes에서 UIWindowScene으로 변환 가능한 객체만 추출한다.
    • 예를 들어, CarPlay ExternalDisplay같은 다른 타입의 장면은 제외하고, 작업하려는 윈도우와 관련된 Scene만 필터링 하는 것이다.

UIWindowScene 배열 순회

  • .forEach로 각 UIWindowScene을 순회하면서, 각 Scene에 속한 윈도우들을 가져온다.
  • windowScene.windows는 해당 Scene의 모든 UIWindow 객체를 배열로 반환하는 것이다.

UIWindow 배열 순회 및 스타일 변경

  • Scene의 윈도우 배열에서 .forEach로 개별 UIWindow 객체를 순회한다.
  • UIWindow에는 overrideUserInterfaceStyle이라는 속성이 있다. 이 속성을 통해 윈도우의 사용자 인터페이스 스타일(라이트/다크 모드)을 강제로 지정할 수 있다.
  • 따라서 UIWindowoverrideUserInterfaceStyle 속성을 통해 강제로 각각 .dark 또는 .light를 적용한 것이다.
  • 만약 시스템 기본값을 따르도록 하려면 .unspecified 를 사용할 수도 있다.

Scene의 구분

위 메서드에서, 명시적으로 Scene을 구분하거나 특정 Scene에 대해 다른 처리를 수행하는 로직 없이 모든 활성화 된 Scene의 UIWindowScene을 추출 후 그 안에 포함된 모든 UIWindow에 대해 스타일을 적용하고 있다.

코드의 목적은 앱의 모든 활성 Scene과 그 Scene에 속한 UIWindow 객체에 동일한 스타일(다크 모드/라이트 모드)을 적용하는 것이기 때문이다.

만약 각 Scene의 구분이 필요하다면 특정 Scene을 다루기 위한 조건문이나 필터링 로직이 추가로 필요할 것이다.

// 예시
UIApplication.shared.connectedScenes
    .compactMap { $0 as? UIWindowScene }
    .filter { $0.session.configuration.name == "MainScene" } // 특정 Scene 필터링
    .forEach { windowScene in
        windowScene.windows.forEach { window in
            window.overrideUserInterfaceStyle = .light
        }
    }

고려해야 하는 상황

멀티 Scene을 고려해야 하는 경우는 주로

  • 외부 디스플레이(예: AirPlay)로 앱의 일부를 출력할 때
  • iPad의 멀티 윈도우
  • CarPlay로 자동차 디스플레이에 실행될 때

의 경우들이다.

초급 단계의 앱 개발에서, iPhone 앱은 대부분 단일 Scene에 단일 UIWindow를 사용하기도 하고, iPhone에서는 기본적으로 멀티 Scene 기능을 사용하지 않으므로, 단일 Scene과 UIWindow만 처리하면 충분하다.

현재까지 다뤄본 각종 UI들은 루트 UIView에 속하고, 이 루트 UIView는 UIViewController가 관리하는 형태였기 때문에, 위의 updateMode 메서드의 forEach문은 한번만 실행 될 것이다.

다만 iOS 15 버전 이상에서 권장하는 방식이기도 하고, 언제까지고 초급 단계에만 머물러 있을 것은 아니기 때문에 이번 기회에 알고 넘어가면 좋다고 생각한다.

참고

0개의 댓글