【SwiftUI】CardView のような GroupBox は本当に便利なのか

こういうやつが簡単に作れる。

👉 GroupBox | Apple Developer Documentation hatena-bookmark


GroupBox("Today's Menu") {
  VStack(alignment: .leading) {
    Text(" curry and rice")
    Text(" green salad")
  }
}
.frame(width: 300)

 

🧑🏻‍💻 作ってみた

手作りと比較します。




VStack {
  HStack {
    Text("Today's Menu")
      .bold()
    Spacer()
  }
  VStack(alignment: .leading) {
    Text(" curry and rice")
    Text(" green salad")
  }
}
.padding()
.frame(width: 300)
.background(
  .background.secondary,
  in: .rect(cornerRadius: 8)
)

かなりコード量に差が出ますね。

 

🧑🏻‍💻 まとめ

便利ですね。

厳しいレイアウトの制限がない限り、

使う機会は多いかもしれません。

ちなみに、macOS で見てみると、

手書きのほうが意図通りとなりました。

少し残念。

👉 【SwiftUI】Default background Color in built-in View Component hatena-bookmark
👉 【SwiftUI】市松模様を背景にする - Checkered Pattern Background 🏁 hatena-bookmark
👉 【SwiftUI】四角の角を丸くする方法あれこれ hatena-bookmark
👉 【SwiftUI】 iOS / macOS の レイアウト記述を typealias で切り替える hatena-bookmark


【SwiftUI】市松模様を背景にする - Checkered Pattern Background 🏁

既存 View の 背景色。

分からないときありません ?

白なのか、グレーなのか、

透過しているのか、マテリアル的なやつなのか。

 

🏁 市松模様 Checkered Pattern

作っておきます。


struct CheckeredPattern: View {
  var size: CGFloat

  var body: some View {
    GeometryReader { gr in
      Grid(horizontalSpacing: 0, verticalSpacing: 0) {
        ForEach(0 ..< Int(ceil(gr.size.height / size)), id: \.self) { y in
          GridRow {
            ForEach(0 ..< Int(ceil(gr.size.width / size)), id: \.self) { x in
              (x % 2 == y % 2 ? Color.gray : Color.white)
                .frame(width: size, height: size)
                .opacity(0.25)
            }
          }
        }
      }
    }
  }
}

🏁 市松模様 - Checkered Pattern

 

🏁 Preview で使う


struct BackgroundCheckeredPattern<Content: View>: View {
  var size: CGFloat
  @ViewBuilder var content: () -> Content

  var body: some View {
    ZStack {
      CheckeredPattern(size: size)
        .edgesIgnoringSafeArea(.all)
      content()
    }
  }
}

 

🏁 extension 化

Preview などで使いやすように extension にしておきます。


extension View {
  func backgroundCheckeredPattern(size: CGFloat) -> some View {
    ZStack {
      CheckeredPattern(size: size)
        .edgesIgnoringSafeArea(.all)
      self
    }
  }
}

特に modifier まで作ることはないですね。


Button("Button") {
}
.buttonStyle(.bordered)
.controlSize(.extraLarge)
.backgroundCheckeredPattern(size: 15) // *

🏁 市松模様 - Checkered Patter

 

🏁 まとめ

サンプルコードとして Gist 化しておきます。


import SwiftUI
struct BackgroundCheckeredPattern<Content: View>: View {
var size: CGFloat = 10
@ViewBuilder var content: () -> Content
var body: some View {
ZStack {
CheckeredPattern(size: size)
.edgesIgnoringSafeArea(.all)
content()
}
}
}
extension View {
func backgroundCheckeredPattern(size: CGFloat) -> some View {
ZStack {
CheckeredPattern(size: size)
.edgesIgnoringSafeArea(.all)
self
}
}
}
struct CheckeredPattern: View {
var size: CGFloat
var body: some View {
GeometryReader { gr in
Grid(horizontalSpacing: 0, verticalSpacing: 0) {
ForEach(0 ..< Int(ceil(gr.size.height / size)), id: \.self) { y in
GridRow {
ForEach(0 ..< Int(ceil(gr.size.width / size)), id: \.self) { x in
(x % 2 == y % 2 ? Color.gray : Color.white)
.frame(width: size, height: size)
.opacity(0.25)
}
}
}
}
}
}
}
#Preview("embedded") {
BackgroundCheckeredPattern(size: 10) { // *
List{
Text("林")
Text("松田")
.listRowBackground(Color.clear)
Text("新井")
Text("高山")
Text("西田")
Text("今村")
}
.scrollContentBackground(.hidden)
}
}
#Preview("extension") {
List{
Text("林")
Text("松田")
.listRowBackground(Color.clear)
Text("新井")
Text("高山")
Text("西田")
Text("今村")
}
.scrollContentBackground(.hidden)
.backgroundCheckeredPattern(size: 15) // *
}

List の背景って .scrollContentBackground(.hidden) で消すんですね。

List の背景って .scrollContentBackground(.hidden) で消す

便利に使えるコードはたくさん持っておきたいです。


【SwiftUI】枠線付き角丸ボタンを簡単に作りたい

しみじみ勉強してきたのに、


なぜか簡単に作れないこういうボタン。

ので、少しやってみました。



 

🧑🏻‍💻 まず思いついた記述

便利な BorderdProminentButtonStyle().overlay() で 角丸枠線をのせます。


// borderProminent + overlay

Button {
} label: {
  VStack {
    Label("BorderedProminent", systemImage: "face.smiling")
    Text(".overlay()")
      .font(.caption)
  }
  .padding()
}
.buttonStyle(.borderedProminent)
.tint(.orange)
#if os(macOS)
.clipShape(.rect(cornerRadius: 16))
#endif
.overlay(RoundedRectangle(cornerRadius: 16).stroke(.gray))

macOS では角がずれる。

ので、clipShape() しました。

気持ち悪いですね。

 

🧑🏻‍💻 よくある記述

この書き方が多いらしいです。

ラベル の View に modifier のチェイン。


// plain + label view modifiers
      
Button {
} label: {
  VStack {
    Label("Plain", systemImage: "face.smiling")
    Text(".background(.orange, in: .rect(cornerRadius: 16))")
      .font(.caption)
    Text(".overlay()")
      .font(.caption)
  }
  .foregroundStyle(.white)
  .padding()
  .background(.orange, in: .rect(cornerRadius: 16))
  .overlay(RoundedRectangle(cornerRadius: 16).stroke(.gray))
}
.buttonStyle(.plain)

ButtonStyle は .plain 一択でしたが、

見た目も、ボタンを押したときの挙動も、

問題ないように見えます。

ただ、コードが長ったらしくなるので、

同様なボタンの数が増えると見通しが悪くなりそうです。

 

🧑🏻‍💻 カスタム ButtonStyle

ビルトインにあっても良さそうなのに。

押したときエフェクトは、BorderdProminentButtonStyle() になるべく似せて ButtonStyle を作ります。


struct StrokeRoundedRectangleButtonStyle: ButtonStyle {
  var cornerRadius: CGFloat

  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .foregroundStyle(
        .white.opacity(configuration.isPressed ? 0.75 : 1)
      )
      .padding()
      .background(
        .orange.opacity(configuration.isPressed ? 0.75 : 1),
        in: .rect(cornerRadius: cornerRadius)
      )
      .overlay(
        RoundedRectangle(cornerRadius: cornerRadius)
          .stroke(.gray.opacity(configuration.isPressed ? 0.75 : 1))
      )
  }
}

extension ButtonStyle where Self == StrokeRoundedRectangleButtonStyle {
  static var strokeRounded: Self { Self(cornerRadius: 16) }
}

ButtonStyle を作ってしまえば、あとは簡単にボタンに適用できます。


// custom style

Button {
} label: {
  VStack {
    Label("Custom Button Style", systemImage: "face.smiling")
    Text("extension ")
      .font(.caption)
  }
}
.buttonStyle(.strokeRounded)

しかし、コード量は多い。

 

🧑🏻‍💻 まとめ

レイアウトの重なりの考え方として、この場合は、

角を丸くするのは .background() で、

枠線はそれに合わせて .overlay() する、

のが良さげに思えました。

あと、

iOS と macOS のコード共用は、シビアなレイアウトになると厳しいのですね!

import SwiftUI
struct TestButton: View {
var body: some View {
VStack(spacing: 16) {
Button {
} label: {
Label("Default", systemImage: "face.smiling")
}
Button {
} label: {
Label("Plain", systemImage: "face.smiling")
}
.buttonStyle(.plain)
Button {
} label: {
Label("Borderless", systemImage: "face.smiling")
}
.buttonStyle(.borderless)
Button {
} label: {
Label("Bordered", systemImage: "face.smiling")
}
.buttonStyle(.bordered)
Button {
} label: {
Label("BorderedProminent", systemImage: "face.smiling")
.foregroundColor(.white)
.padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10))
.background(
RoundedRectangle(cornerRadius: 6)
.foregroundColor(.blue)
)
.compositingGroup()
// .shadow(radius: 5, x: 0, y: 3)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Button {
} label: {
Label("BorderedProminent", systemImage: "face.smiling")
}
.buttonStyle(.borderedProminent)
Button {
} label: {
VStack {
Label("Plain", systemImage: "face.smiling")
Text(".background(.orange)")
.font(.caption)
Text(".containerShape(.rect(cornerRadius: 16))")
.font(.caption)
}
.foregroundStyle(.white)
.padding()
.background(.orange)
.containerShape(.rect(cornerRadius: 16))
}
.buttonStyle(.plain)
Button {
} label: {
VStack {
Label("Plain", systemImage: "face.smiling")
Text(".background(.orange, in: .rect(cornerRadius: 16))")
.font(.caption)
}
.foregroundStyle(.white)
.padding()
.background(.orange, in: .rect(cornerRadius: 16))
}
.buttonStyle(.plain)
Button {
} label: {
VStack {
Label("BorderedProminent", systemImage: "face.smiling")
Text(".tint(.orange)")
.font(.caption)
}
.padding()
}
.buttonStyle(.borderedProminent)
.tint(.orange)
// plain + label view modifiers
Button {
} label: {
VStack {
Label("Plain", systemImage: "face.smiling")
Text(".background(.orange, in: .rect(cornerRadius: 16))")
.font(.caption)
Text(".overlay()")
.font(.caption)
}
.foregroundStyle(.white)
.padding()
.background(.orange, in: .rect(cornerRadius: 16))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(.gray))
}
// default // NG
.buttonStyle(.plain) // OK
// .buttonStyle(.bordered) // NG
// .buttonStyle(.borderless) // no effect
// .buttonStyle(.borderedProminent) // NG
// borderProminent + overlay
Button {
} label: {
VStack {
Label("BorderedProminent", systemImage: "face.smiling")
Text(".overlay()")
.font(.caption)
}
.padding()
}
.buttonStyle(.borderedProminent)
.tint(.orange)
#if os(macOS)
//.buttonBorderShape(.roundedRectangle(radius: 16)) // NG
.clipShape(.rect(cornerRadius: 16))
//.containerShape(.rect(cornerRadius: 16)) // NG
#endif
.overlay(RoundedRectangle(cornerRadius: 16).stroke(.gray))
// custom style
Button {
} label: {
VStack {
Label("Custom Button Style", systemImage: "face.smiling")
Text("extension ")
.font(.caption)
}
}
.buttonStyle(.strokeRounded)
}
}
}
struct StrokeRoundedRectangleButtonStyle: ButtonStyle {
var cornerRadius: CGFloat
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(
.white.opacity(configuration.isPressed ? 0.75 : 1)
)
.padding()
.background(
.orange.opacity(configuration.isPressed ? 0.75 : 1),
in: .rect(cornerRadius: cornerRadius)
)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(.gray.opacity(configuration.isPressed ? 0.75 : 1))
)
}
}
extension ButtonStyle where Self == StrokeRoundedRectangleButtonStyle {
static var strokeRounded: Self { Self(cornerRadius: 16) }
}
#Preview("TestButton") {
TestButton()
.padding()
#if os(iOS)
.frame(maxWidth: .infinity, maxHeight: .infinity)
#else
.frame(height: 800)
#endif
.background(
LinearGradient(
gradient: Gradient(colors: [.white, .pink]),
startPoint: .topLeading, endPoint: .bottomTrailing
)
)
}

👉 Customizing the Appearance and Interaction Behavior of Buttons | Fatbobman's Blog hatena-bookmark