// 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, 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