Zenn
📋

macOSのSwiftUI.Formをカスタマイズ

2025/03/23に公開

MacにおけるForm

macOS用のアプリをSwiftUIで開発する際、Formは便利なツールです。
標準アプリと同様の設定画面を容易に作成できる他、現在の設定アプリと似たリスト型の画面を作成できます。この記事では後者で利用されているGroupedFormStyleのカスタマイズ方法を紹介します。
https://developer.apple.com/documentation/swiftui/form

カスタマイズ方法

Formの各セクションはGroupBox(DefaultGroupBoxStyle)が利用されているため、GroupBoxStyleをカスタマイズすることでFormのカスタマイズが可能になります。

GroupBoxStyleの作成

struct CustomGroupBoxStyle: GroupBoxStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.content
    }
}

// 利用例
Form {
    ...
}
.groupBoxStyle(CustomGroupBoxStyle())

CustomGroupBoxStyleFormにつけることでGroupBoxが装飾していた背景と枠が消えます。

デフォルト CustomGroupBox
3つのアイテムが含まれる、2つのセクションからなるリスト。各セクションは丸みがかかり枠線がついている 前述と同じリストで、各セクションの枠線が削除

このように各Sectionのコンテンツは、形の変形や色付け、回転などある程度のカスタマイズをすることができます。しかし、ヘッダーやフッターはGroupBoxで作成されていないためGroupBoxStyleからカスタマイズはできません。
また、同じForm内で2つ以上の異なるGroupBoxStyleをつけることができず、必ずFormに付く形でコードに記述する必要があります。

角丸 レトロゲーム風 大体思いつくことはできる
各セクションを丸くし、丸いフォントを利用 各セクションを四角にし、ドロップダウンの影を右下に追加 セクションの各角を異なる大きさで丸くし、コンテンツを逆さまに変更

サンプルコード

import SwiftUI

struct ContentView: View {
    var body: some View {
        Form {
            Section("Header 1") {
                ForEach(1..<4) { i in
                    Text(String(repeating: "\(i)", count: i))
                }
            }
            
            Section {
                ForEach(4..<7) { i in
                    Text(String(repeating: "\(i)", count: i))
                }
            } header: {
                Text("Header 2")
            }
        }
        .formStyle(.grouped)
        .groupBoxStyle(.custom)
    }
}

struct CustomGroupBoxStyle: GroupBoxStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.content
// rounded
//            .fontDesign(.rounded)
//            .padding(4)
//            .background()
//            .clipShape(.rect(cornerRadius: 16))
//            .overlay {
//                RoundedRectangle(cornerRadius: 16)
//                    .strokeBorder()
//                    .foregroundStyle(Color(.separatorColor))
//            }

// retro
//            .background()
//            .shadow(radius: 1, x: 1, y: 1)
//            .overlay {
//                Rectangle()
//                    .strokeBorder()
//                    .foregroundStyle(.secondary)
//            }
//            .fontWidth(.expanded)
//            .background {
//                Rectangle()
//                    .foregroundStyle(.secondary)
//                    .offset(x: 4, y: 4)
//            }

// custom
//            .rotationEffect(.degrees(180), anchor: .center)
//            .overlay {
//                UnevenRoundedRectangle(
//                    topLeadingRadius: 16,
//                    bottomLeadingRadius: 64,
//                    bottomTrailingRadius: 32,
//                    topTrailingRadius: 128,
//                    style: .continuous
//                )
//                .strokeBorder(lineWidth: 4)
//                .foregroundStyle(
//                    .linearGradient(
//                        colors: [.red, .blue],
//                        startPoint: .topLeading,
//                        endPoint: .bottomTrailing
//                    )
//                )
//            }
    }
}

extension GroupBoxStyle where Self == CustomGroupBoxStyle {
    static var custom: Self { Self() }
}

#Preview {
    ContentView()
        .frame(width: 400, height: 400)
}

Discussion

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