Skip to content

Instantly share code, notes, and snippets.

@damirstuhec
Last active June 9, 2022 23:39
Show Gist options
  • Save damirstuhec/e232a8d46b79d6c530f657e1460a75f0 to your computer and use it in GitHub Desktop.
Save damirstuhec/e232a8d46b79d6c530f657e1460a75f0 to your computer and use it in GitHub Desktop.
SwiftUI iOS 15 button design system
//
// 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)
}
}
}
@damirstuhec
Copy link
Author

damirstuhec commented Oct 9, 2021

Preview

Screen Shot 2021-10-11 at 20 12 57

Screen Shot 2021-10-11 at 20 12 47

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment