Sources/Source/Components/Buttons/IconButton.swift (92 lines of code) (raw):

import SwiftUI /// A button which only displays an icon. /// /// Used in situations where space is a premium and extra text results in a cluttered design. public struct IconButton: View { private let icon: Image private let size: ButtonSize private let iconColor: Color private let borderColor: Color private let action: () -> () private var disabled: Bool public init( icon: Image, size: ButtonSize, iconColor: Color, borderColor: Color, disabled: Bool = false, action: @escaping () -> Void ) { self.icon = icon self.size = size self.iconColor = iconColor self.borderColor = borderColor self.disabled = disabled self.action = action } public var body: some View { Button { guard disabled == false else { return } action() } label: { icon .resizable() } .buttonStyle(IconButtonStyle(size: size, isDisabled: disabled, borderColor: borderColor, iconColor: iconColor)) } } /// Custom button style used to style an icon button. /// /// Custom disabled functionality has been used here, rather than the native to ensure the styling of the button in the disabled state is correctly reflected and that no touch events are passed through to the view behind. struct IconButtonStyle: ButtonStyle { private let size: ButtonSize private let isDisabled: Bool private let borderColor: Color private let iconColor: Color @Environment(\.colorScheme) private var colorScheme init(size: ButtonSize, isDisabled: Bool = false, borderColor: Color, iconColor: Color) { self.size = size self.isDisabled = isDisabled self.borderColor = borderColor self.iconColor = iconColor } private var disabledOpacity: CGFloat { return colorScheme == .dark ? 0.4 : 0.2 } func makeBody(configuration: Configuration) -> some View { configuration.label .frame(width: size.iconSize, height: size.iconSize) .foregroundStyle(iconColor) .opacity(isDisabled ? disabledOpacity : 1.0) .padding(size.iconPadding) .background { if configuration.isPressed, isDisabled == false { Circle() .fill(borderColor) } else { Circle() .stroke(borderColor, lineWidth: 1.0) } } } } #Preview { IconButton(icon: Image(.chevronLeftSingle), size: .small, iconColor: .black, borderColor: .gray, disabled: false, action: {}) } // Maps button size to layout values for icon. fileprivate extension ButtonSize { var iconSize: CGFloat { switch self { case .xsmall: return 20 case .small: return 24 case .medium: return 28 } } var iconPadding: CGFloat { switch self { case .xsmall: return 2 case .small: return 6 case .medium: return 8 } } }