Created
January 8, 2026 15:31
-
-
Save jacobsapps/d012397607ce1c9608091ea64cdd7347 to your computer and use it in GitHub Desktop.
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
| // | |
| // DisclosureButton.swift | |
| // DesignSystemWar | |
| // | |
| // Created by Jacob Bartlett on 07/05/2023. | |
| // | |
| import SwiftUI | |
| /// Button component that exposes variants and options via initializer defaults. | |
| struct DisclosureButton: View { | |
| // MARK: - Properties - | |
| private let variant: ButtonVariant | |
| private let color: ButtonColor | |
| private let size: ButtonSize | |
| private let icon: ButtonIcon? | |
| private let label: AnyView | |
| private let action: () -> Void | |
| @Environment(\.isEnabled) private var isEnabled | |
| // MARK: - Init - | |
| /// Creates a design-system button with progressive disclosure defaults. | |
| /// - Parameters: | |
| /// - variant: Visual style for fill, border, or text-only. | |
| /// - color: Semantic color for the button surface. | |
| /// - size: Height and icon scale for the button. | |
| /// - icon: Optional leading or trailing icon. | |
| /// - title: Button title text. | |
| /// - action: Action performed on tap. | |
| init(variant: ButtonVariant = .filled, | |
| color: ButtonColor = .default, | |
| size: ButtonSize = .large, | |
| icon: ButtonIcon? = nil, | |
| title: String, | |
| action: @escaping () -> Void) { | |
| self.variant = variant | |
| self.color = color | |
| self.size = size | |
| self.icon = icon | |
| self.label = AnyView(Text(title)) | |
| self.action = action | |
| } | |
| /// Creates a design-system button with a custom label view. | |
| /// - Parameters: | |
| /// - variant: Visual style for fill, border, or text-only. | |
| /// - color: Semantic color for the button surface. | |
| /// - size: Height and icon scale for the button. | |
| /// - icon: Optional leading or trailing icon. | |
| /// - label: Custom view used as the button label. | |
| /// - action: Action performed on tap. | |
| init(variant: ButtonVariant = .filled, | |
| color: ButtonColor = .default, | |
| size: ButtonSize = .large, | |
| icon: ButtonIcon? = nil, | |
| @ViewBuilder label: () -> some View, | |
| action: @escaping () -> Void) { | |
| self.variant = variant | |
| self.color = color | |
| self.size = size | |
| self.icon = icon | |
| self.label = AnyView(label()) | |
| self.action = action | |
| } | |
| // MARK: - Body - | |
| var body: some View { | |
| Button(action: action, label: { | |
| buttonForVariant | |
| }) | |
| .opacity(isEnabled ? 1 : 0.5) | |
| } | |
| @ViewBuilder | |
| private var buttonForVariant: some View { | |
| switch variant { | |
| case .filled: | |
| filledButton | |
| case .bordered: | |
| borderedButton | |
| case .text: | |
| textButton | |
| } | |
| } | |
| private var textButton: some View { | |
| buttonContent | |
| .foregroundColor(color.mainColor) | |
| .frame(height: size.height) | |
| .contentShape(Rectangle()) | |
| } | |
| private var borderedButton: some View { | |
| buttonContent | |
| .foregroundColor(color.mainColor) | |
| .frame(height: size.height) | |
| .buttonBorder(borderColor: color.mainColor, size: size) | |
| } | |
| private var filledButton: some View { | |
| buttonContent | |
| .foregroundColor(color.textColor) | |
| .frame(height: size.height) | |
| .buttonBackground(fillColor: color.mainColor) | |
| } | |
| private var buttonContent: some View { | |
| ButtonLabelContent(label: label, size: size, icon: icon) | |
| } | |
| } | |
| #Preview("Disclosure Filled") { | |
| let sizes: [ButtonSize] = [.large, .small] | |
| let colors = ButtonColor.standardColors | |
| VStack(spacing: 12) { | |
| ForEach(Array(colors.enumerated()), id: \.offset) { _, color in | |
| ForEach(Array(sizes.enumerated()), id: \.offset) { _, size in | |
| DisclosureButton(variant: .filled, | |
| color: color, | |
| size: size, | |
| icon: .leading(.AppIcon.star), | |
| title: "\(color.label) \(size.label)", | |
| action: {}) | |
| } | |
| } | |
| } | |
| .padding() | |
| .background(Color(.secondarySystemBackground)) | |
| } | |
| #Preview("Disclosure Bordered") { | |
| let sizes: [ButtonSize] = [.large, .small] | |
| let colors = ButtonColor.standardColors | |
| VStack(spacing: 12) { | |
| ForEach(Array(colors.enumerated()), id: \.offset) { _, color in | |
| ForEach(Array(sizes.enumerated()), id: \.offset) { _, size in | |
| DisclosureButton(variant: .bordered, | |
| color: color, | |
| size: size, | |
| icon: .trailing(.AppIcon.bookmark), | |
| title: "\(color.label) \(size.label)", | |
| action: {}) | |
| } | |
| } | |
| } | |
| .padding() | |
| .background(Color(.secondarySystemBackground)) | |
| } | |
| #Preview("Disclosure Text") { | |
| let sizes: [ButtonSize] = [.large, .small] | |
| let colors = ButtonColor.standardColors | |
| VStack(spacing: 12) { | |
| ForEach(Array(colors.enumerated()), id: \.offset) { _, color in | |
| ForEach(Array(sizes.enumerated()), id: \.offset) { _, size in | |
| DisclosureButton(variant: .text, | |
| color: color, | |
| size: size, | |
| icon: .leading(.AppIcon.flame), | |
| title: "\(color.label) \(size.label)", | |
| action: {}) | |
| } | |
| } | |
| } | |
| .padding() | |
| .background(Color(.secondarySystemBackground)) | |
| } | |
| #Preview("Disclosure Custom Label") { | |
| DisclosureButton(variant: .bordered, | |
| color: .accent, | |
| size: .large, | |
| icon: .leading(.AppIcon.sparkles), | |
| label: { | |
| HStack(spacing: 6) { | |
| Text("Custom") | |
| Text("Label") | |
| .bold() | |
| } | |
| }, | |
| action: {}) | |
| .padding() | |
| .background(Color(.secondarySystemBackground)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment