Skip to main content
  1. Posts/

SwiftUIのStyleの作り方 - 共通のUIデザインはStyleに切り出す

ボタンやラベルの共通デザインをViewクラスとして切り出して作ることもできますが、SwiftUIではStyleとして切り出す方が都合がよさそうだなと感じています。

クラスとして切り出す場合の問題点 #

例えば、下の画像のようなボタンUIを、クラスで切り出すとします。この場合は、例えば CustomButton などの適当な名前でViewを切り出しつつ、それを使用するViewクラスでは CustomButton のイニシャライザを呼び出して使用します。

// 切り出したUI
struct CustomButton: View {
    let title: LocalizedStringKey
    let action: () -> Void

    var body: some View {
        Button(action: action, label: {
            Text(title)
                .padding()
        })
        .font(.system(size: 17, weight: .bold))
        .foregroundColor(.white)
        .background {
            RoundedRectangle(
                cornerSize: .init(width: 8, height: 8),
                style: .continuous
            )
            .fill(.green)
        }
    }
}

// 使用する側
struct ContentView: View {
    var body: some View {
        VStack {
            CustomButton(title: "Custom", action: executeSomething)
        }
        .padding()
    }

    private func executeSomething() {}
}

こうすることで便利になったようにも感じますが、不都合があります。イニシャライザが固定されてしまいます。Button自体には、状況に応じて使えるイニシャライザがいくつもありますが、それらを使用できなくなります。

Styleとして切り出すと都合が良い #

Buttonのイニシャライザはそのままに、共通するデザインを切り出すには、Styleとして切り出すと便利です。 上の例をそのままStyleで書き直します。

// カスタムスタイルの定義
struct CustomButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .font(.system(size: 17, weight: .bold))
            .foregroundColor(.white)
            .background {
                RoundedRectangle(
                    cornerSize: .init(width: 8, height: 8),
                    style: .continuous
                )
                .fill(.green)    // .fill(.tint)に変更すると便利
            }
            .opacity(configuration.isPressed ? 0.7 : 1.0)
    }
}

// .buttonStyleで指定できるようにする
extension ButtonStyle where Self == CustomButtonStyle {
    static var custom: CustomButtonStyle {
        CustomButtonStyle()
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Button("Custom", action: executeSomething)
                .buttonStyle(.custom)
        }
        .padding()
    }

    private func executeSomething() {}
}

こうすることで、Buttonのイニシャライザを制限することなく、デザインを変更することができます。

応用的な使い方 #

tint #

この方法の良さは、Styleの定義中に tint を使えることです。RoundedRectanglefillで指定する色を.fill(.tint)に変更することで、ボタンを使用する側がボタンの色を指定できるようになります。

Button("Custom", action: executeSomething)
    .buttonStyle(.custom)
    .tint(.green)

パラメータ #

Styleも普通のstructなので、プロパティを持つことができます。Styleのイニシャライザをextensionに実装していますが、その初期化時点でパラメータを渡すことができるということです。 すなわち、下のようにextensionを組めば、パラメータを渡すことが可能になります。

struct CustomButtonStyle: ButtonStyle {
    let cornerRadius: Int   // 角丸の大きさを指定できるようにする

    ...
}

extension ButtonStyle where Self == CustomButtonStyle {
    static func custom(cornerRadius value: Int) -> CustomButtonStyle {
        CustomButtonStyle(cornerRadius: value)
    }
}

// 使用する側
struct ContentView: View {
    var body: some View {
        VStack {
            Button("Custom", action: executeSomething)
                .buttonStyle(.custom(cornerRadius: 25))
                .tint(.green)
        }
        ...
}

スタイルにどんどん切り出してViewをスッキリさせよう #

SwiftUIは、ViewのデザインをViewに書くと、すぐにコードの意図が読みづらくなります。 積極的にStyleに切り出せば、コードの意図がわかりやすい状態を保つことができます。 また、UIのプリミティブなイニシャライザも活用できるようになるので、非常に便利です。