🔨

SwiftUI - テーマカラー機能を実装する方法

2022/05/15に公開約5,900字

作るもの

テーマ変更ページを作成し、テーマを選択するとアプリ全体の色が変わる機能を実装します。

完成イメージ

事前準備

テーマカラー用の色を6色用意しました。

完成コード

ContentView.swift
import SwiftUI

struct ContentView: View {
    
    init(){
        let appearance = UINavigationBarAppearance()
        appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
        appearance.backgroundColor = UIColor(Theme.color)
        UINavigationBar.appearance().tintColor = .white
        UINavigationBar.appearance().barStyle = .black
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().standardAppearance = appearance
    }
    
    var body: some View {
        NavigationView {
            VStack {
                Image(systemName: "paintbrush.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .foregroundColor(Theme.color)
                NavigationLink(destination: ThemeColorView(), label: {
                    Text("テーマカラー変更")
                        .foregroundColor(.white)
                        .padding(50)
                        .background(Theme.color)
                        .cornerRadius(10)
                })
            }
            .navigationBarTitle("タイトル", displayMode: .inline)
        }
    }
}
ThemeColorView.swift
import SwiftUI

struct ThemeColorView: View {
    @Environment(\.dismiss) var dismiss
    @State var buttonTapped = false
    
    var body: some View {
        if buttonTapped {
            ContentView()
        } else {
            VStack(spacing: 0){
                ZStack(){
                    HStack(){
                        Button(
                            action: {
                                dismiss()
                            }, label: {
                                Image(systemName: "arrow.backward")
                                    .resizable()
                                    .frame(width: 20, height: 15)
                            }
                        )
                        .tint(.white)
                        .padding(.leading, 20)
                        Spacer()
                    }
                    Text("テーマカラー")
                        .font(.headline)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
                }
                .padding(.vertical, 10)
                .background(Theme.color)
                VStack(spacing: 0) {
                    HStack(spacing: 0) {
                        ForEach(1..<4) { i in
                            Button(action: {
                                UserDefaults.standard.set(i, forKey: "Theme")
                                buttonTapped = true
                            }) {
                                Rectangle()
                                    .foregroundColor(Color("AccentColor\(i)"))
                            }
                        }
                    }
                    HStack(spacing: 0) {
                        ForEach(4..<7) { i in
                            Button(action: {
                                UserDefaults.standard.set(i, forKey: "Theme")
                                buttonTapped = true
                            }) {
                                Rectangle()
                                    .foregroundColor(Color("AccentColor\(i)"))
                            }
                        }
                    }
                }
            }
	    .navigationBarHidden(true)
        }
    }
}
Theme.swift
import SwiftUI

struct Theme {
    static var color: Color {
        let themeNumber = UserDefaults.standard.object(forKey: "Theme") as? Int ?? 1
        return Color("AccentColor\(themeNumber)")
    }
}

ポイント

破壊と生成によるViewの更新

.navigationBarHidden(true)

ここで一度元のナビゲーションバーを消しています。
そして全く新しいナビゲーションバーを生成することで、

init(){
    let appearance = UINavigationBarAppearance()
    appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
    appearance.backgroundColor = UIColor(Theme.color)
    UINavigationBar.appearance().tintColor = .white
    UINavigationBar.appearance().barStyle = .black
    UINavigationBar.appearance().scrollEdgeAppearance = appearance
    UINavigationBar.appearance().standardAppearance = appearance
}

このナビゲーションバーの初期化時の処理が行われ、
変更後のカラーが反映されるようになっています。

なので、.navigationBarHidden(true)を記入せずに元のバーを残したままにすると、

このようになります。。。

偽物のナビゲーションバー

実は、テーマカラー変更ページに遷移した時には既にナビゲーションバーは消えています。
テーマカラー変更ページにあるナビゲーションバーは、

ZStack(){
    HStack(){
        Button(
            action: {
                dismiss()
            }, label: {
                Image(systemName: "arrow.backward")
                    .resizable()
                    .frame(width: 20, height: 15)
            }
        )
        .tint(.white)
        .padding(.leading, 20)
        Spacer()
    }
    Text("テーマカラー")
        .font(.headline)
        .fontWeight(.bold)
        .foregroundColor(.white)
        .frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
}

この箇所で僕が作った偽物です。

複数のカラーを管理する場合

もし、「テーマカラーに合わせて文字色を変えたい」など、1つのテーマに複数のカラーを登録したい場合は、

Theme.swift
static var color: Color {
    let themeNumber = UserDefaults.standard.object(forKey: "Theme") as? Int ?? 1
    return Color("AccentColor\(themeNumber)")
}

このTheme構造体の中にcolorと同じような変数を追加で用意すれば実装できます。

正攻法ではない

SwiftUIでテーマカラーを実装する方法はなかなか見つからなくて、試行錯誤した結果「破壊と生成でViewを更新する」という正攻法ではない感じのものを思いつきました。

SwiftではviewWillAppearという便利なメソッドがあるので簡単に実装できるのですが、SwiftUIの「.onAppear」は「dismiss」などの戻る系の処理では呼び出されず、簡単にはできませんでした。

https://zenn.dev/musa/articles/0613acc50c56c2

Discussion

ログインするとコメントできます