diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift index 1543e844..ce9e600f 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift @@ -15,14 +15,24 @@ struct CardPreview: View { SUCard(model: self.model, content: self.suCardContent) } Form { + AnimationScalePicker(selection: self.$model.animationScale) Picker("Background Color", selection: self.$model.backgroundColor) { - Text("Default").tag(Optional.none) + Text("Background").tag(UniversalColor.background) Text("Secondary Background").tag(UniversalColor.secondaryBackground) Text("Accent Background").tag(UniversalColor.accentBackground) Text("Success Background").tag(UniversalColor.successBackground) Text("Warning Background").tag(UniversalColor.warningBackground) Text("Danger Background").tag(UniversalColor.dangerBackground) } + Picker("Border Color", selection: self.$model.borderColor) { + Text("Divider").tag(UniversalColor.divider) + Text("Primary").tag(UniversalColor.primary) + Text("Accent").tag(UniversalColor.accent) + Text("Success").tag(UniversalColor.success) + Text("Warning").tag(UniversalColor.warning) + Text("Danger").tag(UniversalColor.danger) + Text("Custom").tag(UniversalColor.universal(.uiColor(.systemPurple))) + } BorderWidthPicker(selection: self.$model.borderWidth) Picker("Content Paddings", selection: self.$model.contentPaddings) { Text("12px").tag(Paddings(padding: 12)) @@ -39,6 +49,7 @@ struct CardPreview: View { Text("Large").tag(Shadow.large) Text("Custom").tag(Shadow.custom(20.0, .zero, UniversalColor.accentBackground)) } + Toggle("Tappable", isOn: self.$model.isTappable) } } } diff --git a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift index a6baf586..2103b564 100644 --- a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift +++ b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift @@ -2,8 +2,16 @@ import Foundation /// A model that defines the appearance properties for a card component. public struct CardVM: ComponentVM { + /// The scaling factor for the card's tap animation, with a value between 0 and 1. + /// + /// Defaults to `.medium`. + public var animationScale: AnimationScale = .medium + /// The background color of the card. - public var backgroundColor: UniversalColor? + public var backgroundColor: UniversalColor = .background + + /// The border color of the card. + public var borderColor: UniversalColor = .divider /// The border thickness of the card. /// @@ -20,6 +28,11 @@ public struct CardVM: ComponentVM { /// Defaults to `.medium`. public var cornerRadius: ContainerRadius = .medium + /// A Boolean value indicating whether the card should allow to be tapped. + /// + /// Defaults to `true`. + public var isTappable: Bool = false + /// The shadow of the card. /// /// Defaults to `.medium`. @@ -28,11 +41,3 @@ public struct CardVM: ComponentVM { /// Initializes a new instance of `CardVM` with default values. public init() {} } - -// MARK: - Helpers - -extension CardVM { - var preferredBackgroundColor: UniversalColor { - return self.backgroundColor ?? .background - } -} diff --git a/Sources/ComponentsKit/Components/Card/SUCard.swift b/Sources/ComponentsKit/Components/Card/SUCard.swift index ad6be3dd..368db91a 100644 --- a/Sources/ComponentsKit/Components/Card/SUCard.swift +++ b/Sources/ComponentsKit/Components/Card/SUCard.swift @@ -16,8 +16,14 @@ public struct SUCard: View { /// A model that defines the appearance properties. public let model: CardVM + /// A closure that is triggered when the card is tapped. + public var onTap: () -> Void + + /// A Boolean value indicating whether the card is pressed. + @State public var isPressed: Bool = false @ViewBuilder private let content: () -> Content + @State private var contentSize: CGSize = .zero // MARK: - Initialization @@ -28,10 +34,12 @@ public struct SUCard: View { /// - content: The content that is displayed in the card. public init( model: CardVM = .init(), - content: @escaping () -> Content + content: @escaping () -> Content, + onTap: @escaping () -> Void = {} ) { self.model = model self.content = content + self.onTap = onTap } // MARK: - Body @@ -39,12 +47,35 @@ public struct SUCard: View { public var body: some View { self.content() .padding(self.model.contentPaddings.edgeInsets) - .background(self.model.preferredBackgroundColor.color) + .background(self.model.backgroundColor.color) .cornerRadius(self.model.cornerRadius.value) .overlay( RoundedRectangle(cornerRadius: self.model.cornerRadius.value) - .stroke(UniversalColor.divider.color, lineWidth: self.model.borderWidth.value) + .stroke( + self.model.borderColor.color, + lineWidth: self.model.borderWidth.value + ) ) .shadow(self.model.shadow) + .observeSize { self.contentSize = $0 } + .simultaneousGesture(DragGesture(minimumDistance: 0.0) + .onChanged { _ in + guard self.model.isTappable else { return } + self.isPressed = true + } + .onEnded { value in + guard self.model.isTappable else { return } + + defer { self.isPressed = false } + + if CGRect(origin: .zero, size: self.contentSize).contains(value.location) { + self.onTap() + } + } + ) + .scaleEffect( + self.isPressed ? self.model.animationScale.value : 1, + anchor: .center + ) } } diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index bae38bde..aa634187 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -21,9 +21,22 @@ open class UKCard: UIView, UKComponent { /// The primary content of the card, provided as a custom view. public let content: Content - // MARK: - Properties + // MARK: - Public Properties - private var contentConstraints = LayoutConstraints() + /// A closure that is triggered when the card is tapped. + public var onTap: () -> Void + + /// A Boolean value indicating whether the button is pressed. + public private(set) var isPressed: Bool = false { + didSet { + self.transform = self.isPressed + ? .init( + scaleX: self.model.animationScale.value, + y: self.model.animationScale.value + ) + : .identity + } + } /// A model that defines the appearance properties. public var model: CardVM { @@ -32,6 +45,10 @@ open class UKCard: UIView, UKComponent { } } + // MARK: - Private Properties + + private var contentConstraints = LayoutConstraints() + // MARK: - Initialization /// Initializer. @@ -41,10 +58,12 @@ open class UKCard: UIView, UKComponent { /// - content: The content that is displayed in the card. public init( model: CardVM = .init(), - content: @escaping () -> Content + content: @escaping () -> Content, + onTap: @escaping () -> Void = {} ) { self.model = model self.content = content() + self.onTap = onTap super.init(frame: .zero) @@ -95,6 +114,8 @@ open class UKCard: UIView, UKComponent { self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath } + // MARK: - Update + /// Updates appearance when the model changes. open func update(_ oldValue: CardVM) { guard self.model != oldValue else { return } @@ -113,6 +134,43 @@ open class UKCard: UIView, UKComponent { // MARK: - UIView Methods + open override func touchesBegan( + _ touches: Set, + with event: UIEvent? + ) { + super.touchesBegan(touches, with: event) + + guard self.model.isTappable else { return } + + self.isPressed = true + } + + open override func touchesEnded( + _ touches: Set, + with event: UIEvent? + ) { + super.touchesEnded(touches, with: event) + + guard self.model.isTappable else { return } + + defer { self.isPressed = false } + + if self.model.isTappable, + let location = touches.first?.location(in: self), + self.bounds.contains(location) { + self.onTap() + } + } + + open override func touchesCancelled( + _ touches: Set, + with event: UIEvent? + ) { + super.touchesCancelled(touches, with: event) + + self.isPressed = false + } + open override func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection? ) { @@ -130,10 +188,10 @@ open class UKCard: UIView, UKComponent { extension UKCard { fileprivate enum Style { static func mainView(_ view: UIView, model: Model) { - view.backgroundColor = model.preferredBackgroundColor.uiColor + view.backgroundColor = model.backgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value view.layer.borderWidth = model.borderWidth.value - view.layer.borderColor = UniversalColor.divider.cgColor + view.layer.borderColor = model.borderColor.cgColor view.shadow(model.shadow) } }