Instantly share code, notes, and snippets.
Forked from damirstuhec/swiftui-ios15-button-design-system.swift
Created
October 10, 2021 17:06
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save codeactual/c37797670a868e1cf83f64247c8f75bc 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 | |
| } | |
| } | |
| } | |
| var type: Type | |
| var size: ControlSize = .regular | |
| var isDisabled = false | |
| var isLoading = false | |
| var scaleWidthToFill = false | |
| @ViewBuilder | |
| func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View { | |
| switch type { | |
| case .plain: | |
| _ButtonBuilderView( | |
| configuration: configuration, | |
| size: size, | |
| isDisabled: isDisabled, | |
| isLoading: isLoading, | |
| scaleWidthToFill: scaleWidthToFill | |
| ) | |
| .buttonStyle(.borderless) | |
| .tint(shouldIgnoreCustomColors(configuration: configuration) ? nil : type.tint) | |
| case .bordered: | |
| _ButtonBuilderView( | |
| configuration: configuration, | |
| size: size, | |
| isDisabled: isDisabled, | |
| isLoading: isLoading, | |
| scaleWidthToFill: scaleWidthToFill | |
| ) | |
| .buttonStyle(.bordered) | |
| .tint(shouldIgnoreCustomColors(configuration: configuration) ? nil : type.tint) | |
| case .prominent: | |
| _ButtonBuilderView( | |
| configuration: configuration, | |
| size: size, | |
| isDisabled: isDisabled, | |
| 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 || isDisabled || isLoading | |
| } | |
| private struct _ButtonBuilderView: View { | |
| let configuration: PrimitiveButtonStyle.Configuration | |
| let size: ControlSize | |
| let isDisabled: Bool | |
| 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(isDisabled || isLoading) | |
| .controlSize(size) | |
| } | |
| } | |
| } | |
| // MARK: - Examples of defining prebuilt button styles using the CustomButtonStyle | |
| extension PrimitiveButtonStyle where Self == CustomButtonStyle { | |
| static func primary(size: ControlSize = .large, isDisabled: Bool = false, isLoading: Bool = false, scaleWidthToFill: Bool = true) -> CustomButtonStyle { | |
| .init( | |
| type: .prominent(customBackground: .yellow, customForeground: .black), | |
| size: size, | |
| isDisabled: isDisabled, | |
| isLoading: isLoading, | |
| scaleWidthToFill: scaleWidthToFill | |
| ) | |
| } | |
| static func secondary(size: ControlSize = .large, isDisabled: Bool = false, isLoading: Bool = false, scaleWidthToFill: Bool = true) -> CustomButtonStyle { | |
| .init( | |
| type: .prominent(customBackground: Color(.secondarySystemFill), customForeground: .primary), | |
| size: size, | |
| isDisabled: isDisabled, | |
| isLoading: isLoading, | |
| scaleWidthToFill: scaleWidthToFill | |
| ) | |
| } | |
| static func toolbar(isDisabled: Bool = false, isLoading: Bool = false) -> CustomButtonStyle { | |
| .init( | |
| type: .plain(customTint: nil), | |
| size: .regular, | |
| isDisabled: isDisabled, | |
| isLoading: isLoading, | |
| scaleWidthToFill: false | |
| ) | |
| } | |
| } | |
| struct ExampleUsageView: View { | |
| var body: some View { | |
| VStack { | |
| Button("Primary button", role: nil, action: { }) | |
| .buttonStyle(.primary()) | |
| Button("Secondary button", role: nil, action: { }) | |
| .buttonStyle(.secondary()) | |
| Button("Toolbar button", role: nil, action: { }) | |
| .buttonStyle(.toolbar()) | |
| } | |
| } | |
| } | |
| // MARK: - Preview | |
| struct ContentView: View { | |
| var body: some View { | |
| ScrollView { | |
| VStack { | |
| Button("Plain default small", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .small, isDisabled: false, isLoading: false)) | |
| Button("Plain default regular", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Plain default large", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .large, isDisabled: false, isLoading: false)) | |
| Button("Plain custom tint", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: .green), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Plain destructive", role: .destructive, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Plain loading", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isDisabled: false, isLoading: true)) | |
| Button("Plain disabled", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .plain(customTint: nil), size: .regular, isDisabled: true, isLoading: false)) | |
| Divider() | |
| } | |
| VStack { | |
| Button("Bordered small", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .small, isDisabled: false, isLoading: false)) | |
| Button("Bordered regular", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Bordered large", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .large, isDisabled: false, isLoading: false)) | |
| Button("Bordered custom tint", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: .green), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Bordered destructive", role: .destructive, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Bordered loading", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isDisabled: false, isLoading: true)) | |
| Button("Bordered disabled", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .bordered(customTint: nil), size: .regular, isDisabled: true, isLoading: false)) | |
| Divider() | |
| } | |
| VStack { | |
| Button("Prominent small", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .small, isDisabled: false, isLoading: false)) | |
| Button("Prominent regular", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Prominent large", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .large, isDisabled: false, isLoading: false)) | |
| Button("Prominent custom tint", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: .primary, customForeground: .green), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Prominent destructive", role: .destructive, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isDisabled: false, isLoading: false)) | |
| Button("Prominent loading", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isDisabled: false, isLoading: true)) | |
| Button("Prominent disabled", role: nil, action: { }) | |
| .buttonStyle(CustomButtonStyle(type: .prominent(customBackground: nil, customForeground: nil), size: .regular, isDisabled: true, isLoading: false)) | |
| } | |
| } | |
| } | |
| } | |
| 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