Instantly share code, notes, and snippets.
-
Save sagaraya/4de22e4bc647f8b617cdca2cb8ebeeb4 to your computer and use it in GitHub Desktop.
SwiftUIのコンポーネントの粒度について考えるために書いたコード
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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