Skip to content

Commit 4597913

Browse files
make card components tappable
1 parent 9d5e14d commit 4597913

File tree

4 files changed

+102
-4
lines changed

4 files changed

+102
-4
lines changed

Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct CardPreview: View {
1515
SUCard(model: self.model, content: self.suCardContent)
1616
}
1717
Form {
18+
AnimationScalePicker(selection: self.$model.animationScale)
1819
Picker("Background Color", selection: self.$model.backgroundColor) {
1920
Text("Background").tag(UniversalColor.background)
2021
Text("Secondary Background").tag(UniversalColor.secondaryBackground)
@@ -48,6 +49,7 @@ struct CardPreview: View {
4849
Text("Large").tag(Shadow.large)
4950
Text("Custom").tag(Shadow.custom(20.0, .zero, UniversalColor.accentBackground))
5051
}
52+
Toggle("Tappable", isOn: self.$model.isTappable)
5153
}
5254
}
5355
}

Sources/ComponentsKit/Components/Card/Models/CardVM.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import Foundation
22

33
/// A model that defines the appearance properties for a card component.
44
public struct CardVM: ComponentVM {
5+
/// The scaling factor for the card's tap animation, with a value between 0 and 1.
6+
///
7+
/// Defaults to `.medium`.
8+
public var animationScale: AnimationScale = .medium
9+
510
/// The background color of the card.
611
public var backgroundColor: UniversalColor = .background
712

@@ -23,6 +28,11 @@ public struct CardVM: ComponentVM {
2328
/// Defaults to `.medium`.
2429
public var cornerRadius: ContainerRadius = .medium
2530

31+
/// A Boolean value indicating whether the card should allow to be tapped.
32+
///
33+
/// Defaults to `true`.
34+
public var isTappable: Bool = false
35+
2636
/// The shadow of the card.
2737
///
2838
/// Defaults to `.medium`.

Sources/ComponentsKit/Components/Card/SUCard.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ public struct SUCard<Content: View>: View {
1616

1717
/// A model that defines the appearance properties.
1818
public let model: CardVM
19+
/// A closure that is triggered when the card is tapped.
20+
public var onTap: () -> Void
21+
22+
/// A Boolean value indicating whether the card is pressed.
23+
@State public var isPressed: Bool = false
1924

2025
@ViewBuilder private let content: () -> Content
26+
@State private var contentSize: CGSize = .zero
2127

2228
// MARK: - Initialization
2329

@@ -28,10 +34,12 @@ public struct SUCard<Content: View>: View {
2834
/// - content: The content that is displayed in the card.
2935
public init(
3036
model: CardVM = .init(),
31-
content: @escaping () -> Content
37+
content: @escaping () -> Content,
38+
onTap: @escaping () -> Void = {}
3239
) {
3340
self.model = model
3441
self.content = content
42+
self.onTap = onTap
3543
}
3644

3745
// MARK: - Body
@@ -49,5 +57,25 @@ public struct SUCard<Content: View>: View {
4957
)
5058
)
5159
.shadow(self.model.shadow)
60+
.observeSize { self.contentSize = $0 }
61+
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
62+
.onChanged { _ in
63+
guard self.model.isTappable else { return }
64+
self.isPressed = true
65+
}
66+
.onEnded { value in
67+
guard self.model.isTappable else { return }
68+
69+
defer { self.isPressed = false }
70+
71+
if CGRect(origin: .zero, size: self.contentSize).contains(value.location) {
72+
self.onTap()
73+
}
74+
}
75+
)
76+
.scaleEffect(
77+
self.isPressed ? self.model.animationScale.value : 1,
78+
anchor: .center
79+
)
5280
}
5381
}

Sources/ComponentsKit/Components/Card/UKCard.swift

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,22 @@ open class UKCard<Content: UIView>: UIView, UKComponent {
2121
/// The primary content of the card, provided as a custom view.
2222
public let content: Content
2323

24-
// MARK: - Properties
24+
// MARK: - Public Properties
2525

26-
private var contentConstraints = LayoutConstraints()
26+
/// A closure that is triggered when the card is tapped.
27+
public var onTap: () -> Void
28+
29+
/// A Boolean value indicating whether the button is pressed.
30+
public private(set) var isPressed: Bool = false {
31+
didSet {
32+
self.transform = self.isPressed
33+
? .init(
34+
scaleX: self.model.animationScale.value,
35+
y: self.model.animationScale.value
36+
)
37+
: .identity
38+
}
39+
}
2740

2841
/// A model that defines the appearance properties.
2942
public var model: CardVM {
@@ -32,6 +45,10 @@ open class UKCard<Content: UIView>: UIView, UKComponent {
3245
}
3346
}
3447

48+
// MARK: - Private Properties
49+
50+
private var contentConstraints = LayoutConstraints()
51+
3552
// MARK: - Initialization
3653

3754
/// Initializer.
@@ -41,10 +58,12 @@ open class UKCard<Content: UIView>: UIView, UKComponent {
4158
/// - content: The content that is displayed in the card.
4259
public init(
4360
model: CardVM = .init(),
44-
content: @escaping () -> Content
61+
content: @escaping () -> Content,
62+
onTap: @escaping () -> Void = {}
4563
) {
4664
self.model = model
4765
self.content = content()
66+
self.onTap = onTap
4867

4968
super.init(frame: .zero)
5069

@@ -95,6 +114,8 @@ open class UKCard<Content: UIView>: UIView, UKComponent {
95114
self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
96115
}
97116

117+
// MARK: - Update
118+
98119
/// Updates appearance when the model changes.
99120
open func update(_ oldValue: CardVM) {
100121
guard self.model != oldValue else { return }
@@ -113,6 +134,43 @@ open class UKCard<Content: UIView>: UIView, UKComponent {
113134

114135
// MARK: - UIView Methods
115136

137+
open override func touchesBegan(
138+
_ touches: Set<UITouch>,
139+
with event: UIEvent?
140+
) {
141+
super.touchesBegan(touches, with: event)
142+
143+
guard self.model.isTappable else { return }
144+
145+
self.isPressed = true
146+
}
147+
148+
open override func touchesEnded(
149+
_ touches: Set<UITouch>,
150+
with event: UIEvent?
151+
) {
152+
super.touchesEnded(touches, with: event)
153+
154+
guard self.model.isTappable else { return }
155+
156+
defer { self.isPressed = false }
157+
158+
if self.model.isTappable,
159+
let location = touches.first?.location(in: self),
160+
self.bounds.contains(location) {
161+
self.onTap()
162+
}
163+
}
164+
165+
open override func touchesCancelled(
166+
_ touches: Set<UITouch>,
167+
with event: UIEvent?
168+
) {
169+
super.touchesCancelled(touches, with: event)
170+
171+
self.isPressed = false
172+
}
173+
116174
open override func traitCollectionDidChange(
117175
_ previousTraitCollection: UITraitCollection?
118176
) {

0 commit comments

Comments
 (0)