swift-buttons/Sources/LilithButtons/IconButton.swift
2026-02-16 09:29:42 -08:00

248 lines
6.9 KiB
Swift
Executable file

// IconButton.swift
// iOS UI Components - Button Components
//
// Icon-only button for compact actions
import SwiftUI
import LilithDesignTokens
/// Icon-only button
///
/// Use for compact actions where space is limited (toolbars, navigation bars).
/// Includes accessibility label requirement.
///
/// Example:
/// ```swift
/// IconButton(icon: Image(systemName: "heart"), label: "Like") {
/// toggleLike()
/// }
/// ```
public struct IconButton: View {
// MARK: - Style
public enum Style {
case filled
case outlined
case ghost
}
public enum Size {
case small // 32pt
case medium // 44pt (default, meets touch target)
case large // 56pt
var dimension: CGFloat {
switch self {
case .small: return 32
case .medium: return 44
case .large: return 56
}
}
var iconSize: CGFloat {
switch self {
case .small: return AppTypography.FontSize.sm
case .medium: return AppTypography.FontSize.base
case .large: return AppTypography.FontSize.lg
}
}
}
// MARK: - Properties
private let icon: Image
private let label: String
private let style: Style
private let size: Size
private let isDisabled: Bool
private let action: () -> Void
// MARK: - State
@State private var isPressed = false
// MARK: - Initialization
/// Create an icon button
/// - Parameters:
/// - icon: SF Symbol or custom image
/// - label: Accessibility label (required)
/// - style: Visual style (filled, outlined, ghost)
/// - size: Button size
/// - isDisabled: Disable button interaction
/// - action: Action to perform on tap
public init(
icon: Image,
label: String,
style: Style = .ghost,
size: Size = .medium,
isDisabled: Bool = false,
action: @escaping () -> Void
) {
self.icon = icon
self.label = label
self.style = style
self.size = size
self.isDisabled = isDisabled
self.action = action
}
// MARK: - Body
public var body: some View {
Button(action: handleTap) {
icon
.font(.system(size: size.iconSize))
.foregroundColor(foregroundColor)
.frame(width: size.dimension, height: size.dimension)
.background(backgroundColor)
.overlay(overlayView)
.clipShape(RoundedRectangle(cornerRadius: AppRadius.md))
.scaleEffect(isPressed ? 0.9 : 1.0)
.animation(AppAnimations.buttonPress, value: isPressed)
}
.disabled(isDisabled)
.buttonStyle(PlainButtonStyle())
.accessibilityLabel(label)
.accessibilityHint("Double tap to activate")
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
isPressed = pressing
}, perform: {})
}
// MARK: - Computed Properties
private var foregroundColor: Color {
if isDisabled {
return AppColors.textTertiary
}
switch style {
case .filled:
return .white
case .outlined, .ghost:
return AppColors.primary
}
}
private var backgroundColor: Color {
if isDisabled {
return style == .filled ? AppColors.Gray.gray700 : Color.clear
}
switch style {
case .filled:
return isPressed ? AppColors.primary.opacity(0.8) : AppColors.primary
case .outlined, .ghost:
return isPressed ? AppColors.primary.opacity(0.1) : Color.clear
}
}
@ViewBuilder
private var overlayView: some View {
if style == .outlined {
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(borderColor, lineWidth: 1.5)
}
}
private var borderColor: Color {
isDisabled ? AppColors.Gray.gray700 : AppColors.primary
}
// MARK: - Methods
private func handleTap() {
guard !isDisabled else { return }
// Haptic feedback
#if canImport(UIKit)
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
#endif
action()
}
}
// MARK: - Preview Provider
#if DEBUG
struct IconButton_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: AppSpacing.xl2) {
// Style variations
HStack(spacing: AppSpacing.lg) {
IconButton(
icon: Image(systemName: "heart"),
label: "Like",
style: .filled
) {
print("Filled tapped")
}
IconButton(
icon: Image(systemName: "heart"),
label: "Like",
style: .outlined
) {
print("Outlined tapped")
}
IconButton(
icon: Image(systemName: "heart"),
label: "Like",
style: .ghost
) {
print("Ghost tapped")
}
}
// Size variations
HStack(spacing: AppSpacing.lg) {
IconButton(
icon: Image(systemName: "star"),
label: "Favorite",
size: .small
) {
print("Small tapped")
}
IconButton(
icon: Image(systemName: "star"),
label: "Favorite",
size: .medium
) {
print("Medium tapped")
}
IconButton(
icon: Image(systemName: "star"),
label: "Favorite",
size: .large
) {
print("Large tapped")
}
}
// Common icons
HStack(spacing: AppSpacing.lg) {
IconButton(icon: Image(systemName: "ellipsis"), label: "More") {}
IconButton(icon: Image(systemName: "square.and.arrow.up"), label: "Share") {}
IconButton(icon: Image(systemName: "bookmark"), label: "Bookmark") {}
IconButton(icon: Image(systemName: "xmark"), label: "Close") {}
}
// Disabled state
HStack(spacing: AppSpacing.lg) {
IconButton(icon: Image(systemName: "heart"), label: "Like", style: .filled, isDisabled: true) {}
IconButton(icon: Image(systemName: "heart"), label: "Like", style: .outlined, isDisabled: true) {}
}
}
.padding()
.previewLayout(.sizeThatFits)
.background(AppColors.background)
.previewDisplayName("Icon Button States")
}
}
#endif