Instantly share code, notes, and snippets.
Last active
June 9, 2022 23:39
-
Star
5
(5)
You must be signed in to star a gist -
Fork
1
(1)
You must be signed in to fork a gist
-
Save damirstuhec/e232a8d46b79d6c530f657e1460a75f0 to your computer and use it in GitHub Desktop.
SwiftUI iOS 15 button design system
This file contains hidden or 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
// | |
// ContentView.swift | |
// SwiftUIDemo | |
// | |
// Created by Damir Stuhec on 07/10/2021. | |
// | |
import SwiftUI | |
// MARK: - CustomButtonStyle | |
struct CustomButtonStyle: PrimitiveButtonStyle { | |
enum `Type` { | |
case plain(customTint: Color?) | |
case bordered(customTint: Color?) | |
case prominent(customBackground: Color?, customForeground: Color?) | |
var tint: Color? { | |
switch self { | |
case .plain(let customTint), .bordered(let customTint), .prominent(let customTint, _): | |
return customTint | |
} | |
} | |
var foreground: Color? { | |
switch self { | |
case .plain, .bordered: | |
return nil | |
case .prominent(_, let customForeground): | |
return customForeground | |
} | |
} | |
} | |
@Environment(\.isEnabled) private var isEnabled: Bool | |
var type: Type | |
var size: ControlSize = .regular | |
var isLoading = false | |
var scaleWidthToFill = false | |
@ViewBuilder | |
func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View { | |
switch type { | |
case .plain: | |
_ButtonBuilderView( | |
configuration: configuration, | |
size: size, | |
isLoading: isLoading, | |
scaleWidthToFill: scaleWidthToFill | |
) | |
.buttonStyle(.borderless) | |
.tint(shouldIgnoreCustomColors(configuration: configuration) ? nil : type.tint) | |
case .bordered: | |
_ButtonBuilderView( | |
configuration: configuration, | |
size: size, | |
isLoading: isLoading, | |
scaleWidthToFill: scaleWidthToFill | |
) | |
.buttonStyle(.bordered) | |
.tint(shouldIgnoreCustomColors(configuration: configuration) ? nil : type.tint) | |
case .prominent: | |
_ButtonBuilderView( | |
configuration: configuration, | |
size: size, | |
isLoading: isLoading, | |
scaleWidthToFill: scaleWidthToFill | |
) | |
.buttonStyle(.borderedProminent) | |
.tint(shouldIgnoreCustomColors(configuration: configuration) ? nil : type.tint) | |
.foregroundColor(shouldIgnoreCustomColors(configuration: configuration) ? nil : type.foreground) | |
} | |
} | |
private func shouldIgnoreCustomColors(configuration: PrimitiveButtonStyle.Configuration) -> Bool { | |
configuration.role != nil || !isEnabled || isLoading | |
} | |
private struct _ButtonBuilderView: View { | |
@Environment(\.isEnabled) private var isEnabled: Bool | |
let configuration: PrimitiveButtonStyle.Configuration | |
let size: ControlSize | |
let isLoading: Bool | |
let scaleWidthToFill: Bool | |
private var font: Font? { | |
switch size { | |
case .mini: | |
return .subheadline.weight(.regular) | |
case .small: | |
return .subheadline.weight(.regular) | |
case .regular: | |
return .body.weight(.medium) | |
case .large: | |
return .body.weight(.semibold) | |
@unknown default: | |
return nil | |
} | |
} | |
var body: some View { | |
Button( | |
role: configuration.role, | |
action: configuration.trigger, | |
label: { | |
configuration.label | |
.font(font) | |
.opacity(isLoading ? 0 : 1) | |
.overlay { | |
if isLoading { | |
ProgressView() | |
} | |
} | |
.frame(maxWidth: scaleWidthToFill ? .infinity : nil) | |
} | |
) | |
.disabled(!isEnabled || isLoading) | |
.controlSize(size) | |
} | |
} | |
} | |
// MARK: - Examples of defining prebuilt button styles using the CustomButtonStyle | |
extension PrimitiveButtonStyle where Self == CustomButtonStyle { | |
static func primary(size: ControlSize = .large, isLoading: Bool = false, scaleWidthToFill: Bool = true) -> CustomButtonStyle { | |
.init( | |
type: .prominent(customBackground: .yellow, customForeground: .black), | |
size: size, | |
isLoading: isLoading, | |
scaleWidthToFill: scaleWidthToFill | |
) | |
} | |
static func secondary(size: ControlSize = .large, isLoading: Bool = false, scaleWidthToFill: Bool = true) -> CustomButtonStyle { | |
.init( | |
type: .prominent(customBackground: Color(.secondarySystemFill), customForeground: .primary), | |
size: size, | |
isLoading: isLoading, | |
scaleWidthToFill: scaleWidthToFill | |
) | |
} | |
static func toolbar(isLoading: Bool = false) -> CustomButtonStyle { | |
.init( | |
type: .plain(customTint: nil), | |
size: .regular, | |
isLoading: isLoading, | |
scaleWidthToFill: false | |
) | |
} | |
} | |
struct ExampleUsageView: View { | |
var body: some View { | |
VStack { | |
Button("Primary button", action: { }) | |
.buttonStyle(.primary()) | |
Button("Secondary button", action: { }) | |
.buttonStyle(.secondary()) | |
Button("Toolbar button", action: { }) | |
.buttonStyle(.toolbar()) | |
} | |
} | |
} | |
// MARK: - Preview | |
struct ContentView: View { | |
var body: some View { | |
ScrollView { | |
VStack { | |
Button("Plain small", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .small, isLoading: false)) | |
Button("Plain regular", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isLoading: false)) | |
Button("Plain large", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .large, isLoading: false)) | |
Button("Plain custom tint", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: .green), size: .regular, isLoading: false)) | |
Button("Plain destructive", role: .destructive, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isLoading: false)) | |
Button("Plain loading", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isLoading: true)) | |
Button("Plain disabled", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isLoading: false)) | |
.disabled(true) | |
Divider() | |
} | |
VStack { | |
Button("Bordered small", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .small, isLoading: false)) | |
Button("Bordered regular", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isLoading: false)) | |
Button("Bordered large", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .large, isLoading: false)) | |
Button("Bordered custom tint", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: .green), size: .regular, isLoading: false)) | |
Button("Bordered destructive", role: .destructive, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isLoading: false)) | |
Button("Bordered loading", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isLoading: true)) | |
Button("Bordered disabled", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isLoading: false)) | |
.disabled(true) | |
Divider() | |
} | |
VStack { | |
Button("Prominent small", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .small, isLoading: false)) | |
Button("Prominent regular", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isLoading: false)) | |
Button("Prominent large", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .large, isLoading: false)) | |
Button("Prominent custom tint", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: .primary, customForeground: .green), size: .regular, isLoading: false)) | |
Button("Prominent destructive", role: .destructive, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isLoading: false)) | |
Button("Prominent loading", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isLoading: true)) | |
Button("Prominent disabled", role: nil, action: { }) | |
.buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isLoading: false)) | |
.disabled(true) | |
} | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
Group { | |
ContentView() | |
.previewLayout(.sizeThatFits) | |
ContentView() | |
.previewLayout(.sizeThatFits) | |
.preferredColorScheme(.dark) | |
ExampleUsageView() | |
.previewLayout(.sizeThatFits) | |
ExampleUsageView() | |
.previewLayout(.sizeThatFits) | |
.preferredColorScheme(.dark) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Preview