Search

다크 모드 토글 버튼 + MVCVM 패턴?

오늘은 앞으로 시작할 프로젝트를 위해 미리 탭 바와 다크 모드만 간단하게 구현해 봤다. 그동안 MVC 패턴만 사용해 왔지만 최근에 MVVM 패턴을 공부하면서 프로젝트가 커질수록 View의 코드가 너무 복잡해지고 무거워지는 것을 느꼈다. 이에 MVC 패턴처럼 View를 다시 View와 Controller로 나누어 MVC with VM 아키텍처 패턴을 시도해 봤다. 인터넷에서 이 아키텍처 패턴에 관한 글을 찾을 수 없었지만 어쩌면 많은 사람들이 이미 이 패턴을 생각했을지도 모른다. 아마 그럴만한 이유가 있어서 채택되지 않았겠지만 내 성격상 직접 부딪쳐보지 않고서는 알 수 없고 이 과정에서 또 새로운 무언가를 발견하고 탄생시킬지도 모른다고 생각한다. 그리고 아직 아키텍처 패턴에 익숙하지 않은 나에겐 좋은 공부법이 될 거라고 생각한다. 또, UserInterfaceStyle을 토글 버튼으로 제어하고 UserDefaults를 활용하여 앱을 재시작해도 설정값과 버튼 상태가 유지되도록 구현하는 방법에 대해 공부했다.

Model

앱의 다크 모드 활성화 여부를 저장하고 불러오는 역할을 담당한다.
import Foundation struct DarkMode { var isDarkModeEnabled: Bool { get { UserDefaults.standard.bool(forKey: "DarkModeEnabled") } set { UserDefaults.standard.set(newValue, forKey: "DarkModeEnabled") } } }
Swift
복사
get절에서는 UserDefaults.standard.bool(forKey: "DarkModeEnabled")를 호출하는 것을 볼 수 있다. 이는 DarkModeEnabled 라는 키를 사용하여 다크 모드가 활성화 되어 있는지의 여부를 Bool 값으로 불러온다. 만약, 해당 키에 대한 값이 설정되어 있지 않으면 false를 반환하는 코드이다. set절에서는 UserDefaults.standard.set(newValue, forKey: "DarkModeEnabled")를 호출하는데 이는 newValue로 전달된 값을 DarkModeEnabled 키에 연결하여 UserDefaults에 저장한다. 이 값은 앱을 종료하고 다시 시작해도 유지된다.

View

코드의 간결성을 위해 CocoaPods을 통해 SnapKit을 들고왔다. UISwitch() 클래스로 스위치 버튼을 쉽게 만들 수 있었다. View 코드는 딱히 볼게 없으니 패스.
import UIKit import SnapKit final class SettingView: UIView { // MARK: - Properties let darkModeSwitchToggleButton = UISwitch() // MARK: - Lifecycle override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Setup UI private func setupUI() { backgroundColor = .background addSubview(darkModeSwitchToggleButton) darkModeSwitchToggleButton.snp.makeConstraints { $0.center.equalTo(self) } } }
Swift
복사

ViewModel

Properties: 다크 모드의 활성화 상태를 관리하는 모델 인스턴스를 설정하고 이 모델 인스턴스의 isDarkModeEnabled 값을 가져와 isDarkModeEnabled라는 변수에 저장한다. Initialization: ViewModel을 초기화할 때 모델의 인스턴스를 내부 속성으로 설정한다. Actions: 사용자가 다크 모드 버튼을 토글할 때 toggleDarkMode 메서드가 호출된다. isDarkModeEnabled 속성 값을 토글하고 updateInterfaceStyle 메서드를 호출하여 UI 스타일을 업데이트한다. Helpers: updateInterfaceStyle 메서드는 연결된 모든 뷰의 UI 스타일을 현재 모드 설정에 맞게 압데이트한다. 그리고 애니메이션 적용 여부는 매개변수를 통해 결정할 수 있도록 했다.
import UIKit final class SettingViewModel { // MARK: - Properties private var darkMode: DarkMode var isDarkModeEnabled: Bool { get { darkMode.isDarkModeEnabled } set { darkMode.isDarkModeEnabled = newValue } } // MARK: - Initialization init(darkMode: DarkMode) { self.darkMode = darkMode } // MARK: - Actions func toggleDarkMode(animated: Bool) { isDarkModeEnabled.toggle() updateInterfaceStyle(animated: animated) } // MARK: - Helpers private func updateInterfaceStyle(animated: Bool) { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } for window in windowScene.windows { if animated { UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve) { window.overrideUserInterfaceStyle = self.isDarkModeEnabled ? .dark : .light } } else { window.overrideUserInterfaceStyle = self.isDarkModeEnabled ? .dark : .light } } } }
Swift
복사
이렇게 뷰 모델은 모델과 뷰 사이에서 데이터를 관리하고 전달하는 역할을 해준다.

Controller

마지막으로 이 코드는 사용자의 다크 모드 설정을 실시간으로 반영하여 앱의 모든 뷰에 적용하여 보여준다. 애니메이션을 통해 부드러운 전환을 제공하여 사용자 경험을 개선시켰다. 그리고 Controller와 View를 분리함으로써 프로퍼티가 가벼워져 코드가 더 깔끔해 보인다.
import UIKit final class SettingController: UIViewController { // MARK: - Properties private let settingView = SettingView() private var viewModel = SettingViewModel(darkMode: DarkMode()) // MARK: - Lifecycle override func loadView() { view = settingView } override func viewDidLoad() { super.viewDidLoad() configureButtonActions() } // MARK: - Actions private func configureButtonActions() { settingView.darkModeSwitchToggleButton.addTarget( self, action: #selector(handleDarkModeSwitchToggle), for: .valueChanged ) initializeDarkModeToggleState() } @objc private func handleDarkModeSwitchToggle(_ sender: UISwitch) { viewModel.toggleDarkMode(animated: true) } // MARK: - Helpers private func initializeDarkModeToggleState() { settingView.darkModeSwitchToggleButton.isOn = viewModel.isDarkModeEnabled } }
Swift
복사

정리

MVC with VM Architecture
Model: UserDefaults를 통해 다크 모드의 활성화 상태를 저장하고 불러오는 역할을 담당
View: UI 구성을 담당
ViewModel: 다크 모드의 활성화 상태를 관리하며 이 상태에 따라 UI 스타일을 업데이트
Controller: 뷰와 뷰 모델 간의 인터랙션을 관리하며 사용자가 다크 모드 스위치를 토글할 때마다 애니메이션과 함께 UI 스타일 반영