[SwiftUI] Button の見た目カスタマイズ ButtonStyle の使い方

SwiftUI

ボタンの見た目をカスタマイズするときに使う ButtonStyle の使い方を説明します。

ボタンのカスタマイズ

ボタンをカスタマイズしようとする時に、大きく2種類のカスタマイズが考えられます。

  • ボタンの見た目をカスタマイズする (例:枠をつける)
  • ボタンの振る舞いをカスタマイズする (例:押されると回転するボタン)

どちらのカスタマイズも、SwiftUI では、.buttonStyle という modifier を使って行うことができるようになってます。

今回は、見た目のカスタマイズを .buttonStyle を使って行う方法を説明します。

ButtonStyle

ボタンの見た目をカスタマイズしたい時に使う ButtonStyle は、名前そのままですが、ButtonStyle という protocol に準拠したものを作ってカスタマイズします。

Apple のドキュメントは、こちら

以下は、iOS 上でデフォルトのスタイルと”ほとんど”同じスタイルになるように作ったスタイルです。

MyButtonStyle ほとんどデフォルト

struct MyButtonStyle: ButtonStyle {
    // (1)
    func makeBody(configuration: Configuration) -> some View {
        MyButton(configuration:configuration)
    }
    // (2)
    struct MyButton: View {
        // (2)
        @Environment(\.isEnabled) var isEnabled
        let configuration: MyButtonStyle.Configuration
        var body: some View {
            // (3)
            configuration.label
                // (4)
                .foregroundColor(isEnabled ? .blue : .gray)
                // (5)
                .opacity(configuration.isPressed ? 0.2 : 1.0)
        }
    }
}
コード解説
  1. makeBody というメソッドを持つことが、ButtonStyle という protocol が要求していることです。ここでは、MyButton という View を返しています。
  2. ボタンが Enable かどうかの情報は、@Environment から取得して判断に使うことができます。ButtonStyle の中からは参照できないため、独自の View を作りその中から参照しています。
  3. makeBody の引数 configuration の label に、与えられたラベルの情報が入っています
  4. Enable かどうかで、フォアグランドカラーを blue と gray で切り替えています
  5. クリックされているかどうかの情報は、configuration.isPressed に入っていて、その情報によって、透明度(Opacity) を変更しています

このように、状態に応じた色や表示する要素等を指定して作っていくことでボタンの見た目をカスタマイズすることができます。

makeBody が返すのは、View であれば良いので、configuration に含まれる label を使わずに、全く別の View を返すことも可能です。

#”ほとんど”同じと言っているのは、クリックされた時のラベルの色がどのくらい透明(Opacity)になっているかの資料が見つからなかったので、目分量で設定しているためです。

見た目をカスタマイズ

ここまでできれば、あとは、 SwiftUI の コンポーネントや modifier の知識を使うだけです。

角の丸い長方形を背景に持つボタンを作るには、以下のような ButtnStyle を定義して、適用すれば良いです。

  • フォアグランド色は、ボタンがEnable であれば .blue、Disable であれば .gray
  • バックグラウンドは、角のRが10の長方形
  • バックグラウンド色は、ボタンがEnable であれば .red.opacity(0.4)、Disable であれば、.white
  • ボタンが押されたら、フォアグランドの opacity を 0.2 にする

上記のようなスタイルを作るには、、以下のように定義して、そのスタイルをボタンに .buttonStyle 指定すれば良いことになります。

MyButtonStyle カスタマイズ例

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        MyButton(configuration:configuration)
    }
    
    struct MyButton: View {
        @Environment(\.isEnabled) var isEnabled
        let configuration: MyButtonStyle.Configuration
        var body: some View {
            configuration.label
                .foregroundColor(isEnabled ? .blue : .gray)
                .opacity(configuration.isPressed ? 0.2 : 1.0)
                .padding(15)
                .background(isEnabled ? Color.red.opacity(0.4) : Color.white)
                .cornerRadius(10)
        }
    }
}

実際に配置すると、以下のような見た目になります。

カスタムButtonStyle 使用例

「カスタムButtonStyle 使用例」
上記のアプリのコードは、以下です。(ButtonStyle 定義も含まれています)

アプリコード

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2020/11/13
//  © 2020  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var buttonDisabled:Bool = false
    var body: some View {
        VStack {
            Spacer()
            Button(action: {print("Hello")}, label: {
                Text("MyButton")
            })
            .buttonStyle(MyButtonStyle())
            .disabled(buttonDisabled)
            .padding()
            
            Spacer()
            
            Button(action: { buttonDisabled.toggle()  }, label: {
                Text("Toggle Button disable/enable")
            })
            .padding()
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        MyButton(configuration:configuration)
    }
    
    struct MyButton: View {
        @Environment(\.isEnabled) var isEnabled
        let configuration: MyButtonStyle.Configuration
        var body: some View {
            configuration.label
                .foregroundColor(isEnabled ? .blue : .gray)
                .opacity(configuration.isPressed ? 0.2 : 1.0)
                .padding(15)
                .background(isEnabled ? Color.red.opacity(0.4) : Color.white)
                .cornerRadius(10)
        }
    }
}

まとめ:Button のカスタマイズ方法 (ButtonStyle を使ったカスタマイズ方法)

Button のカスタマイズ方法 (ButtonStyle を使ったカスタマイズ方法)
  • ButtonStyle に準拠したスタイルを定義することで、スタイルをカスタマイズできる
  • ButtonStyle protocol に準拠するためには、makeBody を実装する必要がある
  • makeBody の返す View が、ボタンとして表示される
  • makeBody の引数 configuration にラベル等のボタン定義時の情報が渡される

SwiftUI本

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
以下の”SwiftUI Views Mastery Bundle"という本がビジュアル的に確認して探せるので、超便利です。

SwiftUIViewsMastery
https://www.bigmountainstudio.com/swiftui-views-book

4 COMMENTS

佐々木

こちらがまだ初心者なんですが、上記のコードを実行したら、下記のエラーは発生しましたが、
お心当たり、何かありませんか。
cannot find type ‘Configuration’ in scope
—————————————-
40:34: error: cannot find type ‘Configuration’ in scope
func makeBody(configuration: Configuration) -> some View {
^~~~~~~~~~~~~
46:42: error: ‘Configuration’ is not a member type of ‘MyButtonStyle’
let configuration: MyButtonStyle.Configuration
~~~~~~~~~~~~~ ^
39:8: error: inheritance from non-protocol type ‘ButtonStyle’
struct MyButtonStyle: ButtonStyle {
^
18:14: error: no exact matches in call to instance method ‘buttonStyle’
.buttonStyle(MyButtonStyle())
^

確認環境
——
macOS 10.15.7
xcode 12.3
swift 5.3

返信する
tyagishi

こんにちは、コメントありがとうございます。

コードの初めに、”import SwiftUI” を追加すると解決しませんでしょうか?

エラーそのものは、Configuration 等が見つからないというエラーなので、SwiftUI を import することで解決しそうです。

ただ、Xcode のエラーは、たまにとんでもないところに無関係なエラーで表示されるのであてにならない時がありますので、別の問題がある可能性も捨てきれません。

なお、以下の環境では コンパイル・動作 ともに問題ありませんでした。
macOS 10.15.3 beta
Xcode 12.4
swift 5.3.2

一度、お試しください。

返信する
佐々木

ご回答ありがとうございました。
原因はButtonStyle名が別の構造体名(Libファイル)に使用されたからです。
因みに、MyButtonStyle()には、引数を追加することが可能でしょうか。
例:MyButtonStyle(width, height)

以上、よろしくお願い致します。

返信する
tyagishi

MyButtonStyle への引数追加は可能です。

MyButtonStyle は、struct MyButtonStyle: ButtonStyle { …. として定義された struct なので、引数を要求する initializer を定義すると引数追加が必要となります。

例えば、以下のように定義すると、MyButtonStyle(Color.red) のような引数が追加された使い方になります。
struct MyButtonStyle: ButtonStyle {
let bgColor: Color
init(_ bgColor: Color) {
self.bgColor = bgColor
}

}

struct では initializer を明示的に定義しなくても、暗黙的に memberwise initializer が定義されるので、それを使うことも可能です。

ご参考まで。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です