🙆

SwiftUIで外観モードを変更する

2024/10/05に公開

本文

SwiftUIを使ったiOSアプリで、外観モードを変える例の紹介です。@AppStorageを使って、UserDefaultsに保存します。次のように、設定画面とAppの両方で@AppStorageを呼び出しておくと、設定ファイルで変更されると、全体の外観モードが切り替わります。tagにはIntよりもUIUserInterfaceStyleを使うと良いと思います。

import SwiftUI

struct AppearanceSettingView: View {
  @AppStorage(wrappedValue: .init(), "storage") var storage: Persisted
  var body: some View {
    List {
      Section(content: {
        Picker("Appearance setting", selection: $storage.colorMode) {
          Text("System")
            .tag(UIUserInterfaceStyle.unspecified)
          Text("Light")
            .tag(UIUserInterfaceStyle.light)
          Text("Dark")
            .tag(UIUserInterfaceStyle.dark)
        }
        .pickerStyle(.segmented)
      }, header: {
        Text("\(Image(systemName: "smartphone")) Mode")
      })
      .listRowInsets(.init(.zero))
      .listRowBackground(Color.clear)
    }
    .headerProminence(.increased)
    .listStyle(.insetGrouped)
    .navigationTitle("Appearance")
    .navigationBarTitleDisplayMode(.inline)
  }
}

UIUserInterfaceStyleColorSchemeが生成できます。.unspecifiedの場合はちゃんとColorSchemenilになります。

import SwiftUI

@main
@MainActor
struct ExampleApp: App {
  @AppStorage(wrappedValue: .init(), "storage") var storage: Persisted

  var body: some Scene {
    WindowGroup {
      MainTabView()
        .preferredColorScheme(ColorScheme(storage.colorMode))
    }
  }
}

Persistedは次のようにします(Persistedも名前を考え直した方が良いでしょう)。実際には、colorMode以外の設定もここに入れたら良いと思います(省略していますが、他の設定も増やした場合は==の定義をカスタムしたほうが良いようです)。

import UIKit

struct Persisted: Codable {
  var colorMode: UIUserInterfaceStyle

  enum CodingKeys: String, CodingKey {
    case colorMode
  }

  init(colorMode: UIUserInterfaceStyle = .unspecified) {
    self.colorMode = colorMode
  }

  init(from decoder: any Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    colorMode = try .init(rawValue: container.decode(Int.self, forKey: .colorMode)) ?? .unspecified
  }

  func encode(to encoder: any Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(colorMode.rawValue, forKey: .colorMode)
  }
}

extension Persisted: RawRepresentable {
  init?(rawValue: String) {
    do {
      self = try JSONDecoder().decode(Self.self, from: Data(rawValue.utf8))
    } catch {
      return nil
    }
  }

  var rawValue: String {
    (try? String(decoding: JSONEncoder().encode(self), as: UTF8.self)) ?? ""
  }
}

参考

https://zenn.dev/noppe/articles/31a61bd3d17891
https://zenn.dev/ikeh1024/articles/bd7dcf28e38f3c
https://developer.apple.com/documentation/swiftui/colorscheme/init(_:)

Discussion