swift-forms/Sources/LilithForms/Checkbox.swift

241 lines
7.2 KiB
Swift
Executable file

// Checkbox.swift
// iOS UI Components - Form Components
//
// Checkbox with label and indeterminate state
import SwiftUI
import LilithDesignTokens
/// Checkbox control
///
/// Checkbox with optional label and indeterminate state support.
///
/// Example:
/// ```swift
/// @State private var isAgreed = false
///
/// Checkbox(
/// "I agree to the terms",
/// isChecked: $isAgreed
/// )
/// ```
public struct Checkbox: View {
// MARK: - CheckboxState
public enum CheckboxState {
case unchecked
case checked
case indeterminate
}
// MARK: - Properties
private let label: String?
@Binding private var isChecked: Bool
private let isIndeterminate: Bool
private let isDisabled: Bool
private let onChange: ((Bool) -> Void)?
// MARK: - State
@State private var isPressed = false
// MARK: - Initialization
/// Create a checkbox
/// - Parameters:
/// - label: Optional label text
/// - isChecked: Binding to checked state
/// - isIndeterminate: Show indeterminate state
/// - isDisabled: Disable interaction
/// - onChange: Callback when checked state changes
public init(
_ label: String? = nil,
isChecked: Binding<Bool>,
isIndeterminate: Bool = false,
isDisabled: Bool = false,
onChange: ((Bool) -> Void)? = nil
) {
self.label = label
self._isChecked = isChecked
self.isIndeterminate = isIndeterminate
self.isDisabled = isDisabled
self.onChange = onChange
}
// MARK: - Body
public var body: some View {
Button(action: handleTap) {
HStack(spacing: AppSpacing.sm) {
// Checkbox box
ZStack {
RoundedRectangle(cornerRadius: AppRadius.xs)
.fill(boxBackgroundColor)
.frame(width: 24, height: 24)
RoundedRectangle(cornerRadius: AppRadius.xs)
.stroke(borderColor, lineWidth: 2)
.frame(width: 24, height: 24)
// Checkmark or dash
if isIndeterminate {
Rectangle()
.fill(checkmarkColor)
.frame(width: 12, height: 2)
} else if isChecked {
Image(systemName: "checkmark")
.font(.system(size: 14, weight: .bold))
.foregroundColor(checkmarkColor)
}
}
.scaleEffect(isPressed ? 0.9 : 1.0)
.animation(AppAnimations.buttonPress, value: isPressed)
// Label
if let label = label {
Text(label)
.font(AppTypography.body())
.foregroundColor(labelColor)
.multilineTextAlignment(.leading)
}
}
}
.disabled(isDisabled)
.buttonStyle(PlainButtonStyle())
.accessibilityElement(children: .combine)
.accessibilityLabel(label ?? "Checkbox")
.accessibilityAddTraits(isChecked ? [.isSelected] : [])
.accessibilityHint("Double tap to \(isChecked ? "uncheck" : "check")")
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
if !isDisabled {
isPressed = pressing
}
}, perform: {})
}
// MARK: - Computed Properties
private var state: CheckboxState {
if isIndeterminate {
return .indeterminate
}
return isChecked ? .checked : .unchecked
}
private var boxBackgroundColor: Color {
if isDisabled {
return AppColors.Gray.gray800
}
if isChecked || isIndeterminate {
return AppColors.primary
}
return AppColors.surface
}
private var borderColor: Color {
if isDisabled {
return AppColors.Gray.gray600
}
if isChecked || isIndeterminate {
return AppColors.primary
}
return AppColors.border
}
private var checkmarkColor: Color {
.white
}
private var labelColor: Color {
isDisabled ? AppColors.textTertiary : AppColors.textPrimary
}
// MARK: - Methods
private func handleTap() {
guard !isDisabled else { return }
isChecked.toggle()
// Light haptic feedback
#if canImport(UIKit)
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
#endif
onChange?(isChecked)
}
}
// MARK: - Preview Provider
#if DEBUG
struct Checkbox_Previews: PreviewProvider {
static var previews: some View {
VStack(alignment: .leading, spacing: AppSpacing.xl) {
// Unchecked
Checkbox("Unchecked", isChecked: .constant(false))
// Checked
Checkbox("Checked", isChecked: .constant(true))
// Indeterminate
Checkbox("Indeterminate", isChecked: .constant(false), isIndeterminate: true)
// Disabled unchecked
Checkbox("Disabled Unchecked", isChecked: .constant(false), isDisabled: true)
// Disabled checked
Checkbox("Disabled Checked", isChecked: .constant(true), isDisabled: true)
// Without label
HStack {
Checkbox(isChecked: .constant(false))
Checkbox(isChecked: .constant(true))
Checkbox(isChecked: .constant(false), isIndeterminate: true)
}
Divider()
// In context - Settings form
VStack(alignment: .leading, spacing: AppSpacing.md) {
Text("Notification Settings")
.font(AppTypography.h3())
.foregroundColor(AppColors.textPrimary)
Checkbox("Email notifications", isChecked: .constant(true))
Checkbox("Push notifications", isChecked: .constant(true))
Checkbox("SMS notifications", isChecked: .constant(false))
Checkbox("Marketing emails", isChecked: .constant(false), isDisabled: true)
}
.padding()
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppRadius.card))
Divider()
// In context - Terms agreement
HStack(alignment: .top, spacing: AppSpacing.sm) {
Checkbox(isChecked: .constant(false))
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text("I agree to the Terms of Service and Privacy Policy")
.font(AppTypography.body())
.foregroundColor(AppColors.textPrimary)
Text("Required to create an account")
.font(AppTypography.caption())
.foregroundColor(AppColors.textSecondary)
}
}
.padding()
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppRadius.card))
}
.padding()
.background(AppColors.background)
.previewDisplayName("Checkbox States")
}
}
#endif