はじめに
iOSには外観モードを通常モードかダークモードに設定できますが、その設定によってアプリで表示する画面やコンポーネントの色も変えたいケースがあると思います。
Sign In With AppleをSwiftUIで実装した時に、以下のようなボタンがデフォルトで表示されるのですが、外観モードをダークモードにしたときは背景も黒くなるため、ボタンの背景色とかぶりボタンの領域がわからないということになります。
これを回避するために、SwiftUIで外観モードによってSign In With Appleのボタンの色を切り替える実装を行なったので、躓いた点を含めて記事にします。
必要環境
- iOS 14以上
検証環境
- iOS 18.0
- Xcode16.4
外観モードの取得
SwiftUIでは外観モードの設定は@Environment(\.colorScheme) var
で環境変数として取得できます。
colorScheme
の定義
colorSchemeの定義
/// Set a preferred appearance for a particular view hierarchy to override
/// the user's Dark Mode setting using the ``View/preferredColorScheme(_:)``
/// view modifier.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public enum ColorScheme : CaseIterable, Sendable {
/// The color scheme that corresponds to a light appearance.
case light
/// The color scheme that corresponds to a dark appearance.
case dark
/// Returns a Boolean value indicating whether two values are equal.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
///
/// - Parameters:
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public static func == (a: ColorScheme, b: ColorScheme) -> Bool
/// A type that can represent a collection of all values of this type.
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public typealias AllCases = [ColorScheme]
/// A collection of all values of this type.
nonisolated public static var allCases: [ColorScheme] { get }
/// Hashes the essential components of this value by feeding them into the
/// given hasher.
///
/// Implement this method to conform to the `Hashable` protocol. The
/// components used for hashing must be the same as the components compared
/// in your type's `==` operator implementation. Call `hasher.combine(_:)`
/// with each of these components.
///
/// - Important: In your implementation of `hash(into:)`,
/// don't call `finalize()` on the `hasher` instance provided,
/// or replace it with a different instance.
/// Doing so may become a compile-time error in the future.
///
/// - Parameter hasher: The hasher to use when combining the components
/// of this instance.
public func hash(into hasher: inout Hasher)
/// The hash value.
///
/// Hash values are not guaranteed to be equal across different executions of
/// your program. Do not save hash values to use during a future execution.
///
/// - Important: `hashValue` is deprecated as a `Hashable` requirement. To
/// conform to `Hashable`, implement the `hash(into:)` requirement instead.
/// The compiler provides an implementation for `hashValue` for you.
public var hashValue: Int { get }
}
具体的には以下のような感じです。
struct SignInUpWithAppleButton: View {
@Environment(\.colorScheme) var colorScheme // 外観モードの取得
var body: some View {
SignInWithAppleButton {
...
} onCompletion: {
...
}
}
}
ボタンの色の変更
今回はSign In With Appleのボタンを例にしているので、ボタンの色の変更はsignInWithAppleButtonStyle(_:)
で設定します。
以下のように、外観モードが通常であれば、ボタンの色を黒に、ダークモードであれば白色にします。
struct SignInUpWithAppleButton: View {
@Environment(\.colorScheme) var colorScheme // 外観モードの取得
var body: some View {
SignInWithAppleButton {
...
} onCompletion: {
...
}
}
.signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
}
上記のようにすると、以下のように外観モードによってボタンの色が変わるようになります。
通常モード | ダークモード |
---|---|
![]() |
![]() |
画面表示中に外観モードを切り替えてもボタンの色が変わらない
現状の実装だと以下のような具合に、画面表示中に外観モードを切り替えてもボタンの色が更新されません。
これは自分の知る限りはsignInWithAppleButtonStyle
の変更に限った話で、Text
やButton
などの色を変える分には問題なく外観モードを切り替えで再描画が走ります。
(多分ちゃんと調べたら他にもあるかもですが)
You receive a color scheme value when you read the colorScheme environment value. The value tells you if a light or dark appearance currently applies to the view. SwiftUI updates the value whenever the appearance changes, and redraws views that depend on the value. For example, the following Text view automatically updates when the user enables Dark Mode:
日本語訳
環境値を読み取ると、カラースキームの値を受け取ります。この値は、現在ビューに明るい外観が適用されているか暗い外観が適用されているかを示します。SwiftUIは外観が変更されるたびに値を更新し、値に依存するビューを再描画します。例えば、次のビューはユーザーがダークモードを有効にすると自動的に更新されます。
ColorSchemeのドキュメントにも、外観モードが変わるたびに値に依存するビューを再描画すると記載があるので、外観モードが変わった時に再描画が走るのが正しい挙動なのでしょう。
再描画にはViewのIdentityが変わると走ると理解しているので、SignInWithAppleButton
の再描画が変わらなかったのもIdentityが変わらなかったのではと思います。
そのため、以下のように明示的にIdentityを変更させてやれば、外観モードを切り替えたら再描画が走るようになりました。
id
にcolorScheme
を指定して、colorScheme
が変更されると明示的にSignInWithAppleButton
のIdentityを変更しています。
struct SignInUpWithAppleButton: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
SignInWithAppleButton {
...
} onCompletion: {
...
}
.signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
.id(colorScheme) // idにcolorSchemeを指定
}
}
おわり
最後の方は結構ニッチな内容になってしまいましたが、自分的にはちょっと学びになりました。
どなたかの参考になれば幸いです。