1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIで外観モードによってViewの色を切り替える

Posted at

はじめに

iOSには外観モードを通常モードかダークモードに設定できますが、その設定によってアプリで表示する画面やコンポーネントの色も変えたいケースがあると思います。

Sign In With AppleをSwiftUIで実装した時に、以下のようなボタンがデフォルトで表示されるのですが、外観モードをダークモードにしたときは背景も黒くなるため、ボタンの背景色とかぶりボタンの領域がわからないということになります。

image.png

これを回避するために、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)
}

上記のようにすると、以下のように外観モードによってボタンの色が変わるようになります。

通常モード ダークモード

画面表示中に外観モードを切り替えてもボタンの色が変わらない

現状の実装だと以下のような具合に、画面表示中に外観モードを切り替えてもボタンの色が更新されません。

Simulator Screen Recording - iPhone 16 - 2025-08-07 at 22.58.34.gif

これは自分の知る限りはsignInWithAppleButtonStyleの変更に限った話で、TextButtonなどの色を変える分には問題なく外観モードを切り替えで再描画が走ります。
(多分ちゃんと調べたら他にもあるかもですが)

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を変更させてやれば、外観モードを切り替えたら再描画が走るようになりました。
idcolorSchemeを指定して、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を指定
    }
}

Simulator Screen Recording - iPhone 16 - 2025-08-07 at 23.21.30.gif

おわり

最後の方は結構ニッチな内容になってしまいましたが、自分的にはちょっと学びになりました。
どなたかの参考になれば幸いです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?