🔨
SwiftUI - テーマカラー機能を実装する方法
作るもの
テーマ変更ページを作成し、テーマを選択するとアプリ全体の色が変わる機能を実装します。
完成イメージ
事前準備
テーマカラー用の色を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」などの戻る系の処理では呼び出されず、簡単にはできませんでした。
Discussion