Skip to content

Instantly share code, notes, and snippets.

@sagaraya
Created August 19, 2021 03:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sagaraya/4de22e4bc647f8b617cdca2cb8ebeeb4 to your computer and use it in GitHub Desktop.
Save sagaraya/4de22e4bc647f8b617cdca2cb8ebeeb4 to your computer and use it in GitHub Desktop.
SwiftUIのコンポーネントの粒度について考えるために書いたコード
import SwiftUI
/// ページ内の主要なアクションに用いるボタン
struct PrimaryButton: View {
enum Style {
enum Size {
case regular
case compact
var height: CGFloat {
switch self {
case .regular: return 50
case .compact: return 32
}
}
}
/// ボタンの背景色をparimaryColorにしたstyle
case `default`(Size)
/// ボタンの背景色をwhiteにしたstyle
/// defaultだと視認性が悪い場合に使う
case inverted(Size)
}
enum State {
case disabled
case enabled
}
let text: String
let style: Style
@Binding var state: State
let action: () -> Void
init(text: String, style: Style = .default(.regular), state: Binding<State>, action: @escaping () -> Void) {
self.text = text
self.style = style
_state = state
self.action = action
}
var body: some View {
Button(action: action) {
Text(text)
}
.buttonStyle(PrimaryButtonStyle(style: style, state: $state))
.disabled(state == .disabled)
}
}
struct PrimaryButtonStyle: ButtonStyle {
let style: PrimaryButton.Style
@Binding var state: PrimaryButton.State
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.system(size: 15, weight: .bold))
.lineLimit(1)
.variant(for: style, isPressed: configuration.isPressed)
.opacity(state == .disabled ? 0.7 : 1)
}
/// コンポーネントの見た目のうち、styleによって変わる部分
enum Variant {
struct Default: ViewModifier {
var isPressed: Bool
func body(content: Content) -> some View {
content
.foregroundColor(Color.white)
.background(isPressed ? Color(UIColor.primary.darken()) : Color(UIColor.primary))
}
}
struct Inverted: ViewModifier {
var isPressed: Bool
func body(content: Content) -> some View {
content
.foregroundColor(Color(UIColor.primary))
.background(isPressed ? Color(UIColor.white.darken()) : Color(UIColor.white))
}
}
enum Size {
struct Regular: ViewModifier {
let size: PrimaryButton.Style.Size
func body(content: Content) -> some View {
content
.padding(EdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32))
.frame(height: size.height) // fixed height
.frame(minWidth: 225) // variable width
}
}
struct Compact: ViewModifier {
let size: PrimaryButton.Style.Size
func body(content: Content) -> some View {
content
.padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
.frame(height: size.height) // fixed height
}
}
}
}
}
private extension View {
typealias Variant = PrimaryButtonStyle.Variant
@ViewBuilder
func variant(for style: PrimaryButton.Style, isPressed: Bool) -> some View {
switch style {
case let .default(size):
// NOTE: size -> 色 → cornerRadiusの順で指定する必要がある
variant(for: size)
.modifier(Variant.Default(isPressed: isPressed))
.cornerRadius(size.height / 2)
case let .inverted(size):
variant(for: size)
.modifier(Variant.Inverted(isPressed: isPressed))
.cornerRadius(size.height / 2)
}
}
@ViewBuilder
private func variant(for size: PrimaryButton.Style.Size) -> some View {
switch size {
case .regular:
modifier(Variant.Size.Regular(size: size))
case .compact:
modifier(Variant.Size.Compact(size: size))
}
}
}
struct PrimaryButton_Previews: PreviewProvider {
static var previews: some View {
// default style
Group {
PrimaryButton(text: "ログイン", state: .constant(.enabled), action: {})
.previewDisplayName("default - regular / enabled")
PrimaryButton(text: "ログイン", state: .constant(.disabled), action: {})
.previewDisplayName("default - reaular / disabled")
PrimaryButton(text: "完了", style: .default(.compact), state: .constant(.enabled), action: {})
.previewDisplayName("default - compact / enabled")
PrimaryButton(text: "完了", style: .default(.compact), state: .constant(.disabled), action: {})
.previewDisplayName("default - compact / disabled")
}
.previewLayout(.sizeThatFits)
// inverted style
Group {
PrimaryButton(text: "ログイン", style: .inverted(.regular), state: .constant(.enabled), action: {})
.previewDisplayName("inverted - regular / enabled")
PrimaryButton(text: "ログイン", style: .inverted(.regular), state: .constant(.disabled), action: {})
.previewDisplayName("inverted - regular / disabled")
PrimaryButton(text: "完了", style: .inverted(.compact), state: .constant(.enabled), action: {})
.previewDisplayName("inverted - compact / enabled")
PrimaryButton(text: "完了", style: .inverted(.compact), state: .constant(.disabled), action: {})
.previewDisplayName("inverted - compact / disabled")
}
.background(Color(UIColor.primary))
.previewLayout(.sizeThatFits)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment