Skip to content

Commit eb4e11a

Browse files
Expose largeContentViewerInteraction on LargeContentViewer backing view via protocol (#583)
We need access to the `largeContentViewerInteraction` on the backing view so that, if a view supports a long press gesture, it's able to support both the large content viewer as well as the existing action, per [this](https://developer.apple.com/videos/play/wwdc2019/261/) WWDC session. This change exposes that property via a public protocol. ### Demo Observe the colours on the "Long press large content" button. https://github.com/user-attachments/assets/6893fbb5-5edf-425c-b39d-c4bc3c7631fc
2 parents 624fe0e + 3752fc0 commit eb4e11a

File tree

3 files changed

+120
-2
lines changed

3 files changed

+120
-2
lines changed

BlueprintUICommonControls/Sources/AccessibilityLargeContentViewer.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,11 @@ extension Accessibility {
212212
}
213213

214214
extension Accessibility {
215+
public protocol LargeContentViewerInteractionContainerViewable: UIView {
216+
var largeContentViewerInteraction: UILargeContentViewerInteraction? { get }
217+
}
215218

216-
private final class LargeContentViewerInteractionContainerView: UIView, UILargeContentViewerInteractionDelegate {
219+
private final class LargeContentViewerInteractionContainerView: UIView, LargeContentViewerInteractionContainerViewable, UILargeContentViewerInteractionDelegate {
217220

218221
var largeContentViewerInteraction: UILargeContentViewerInteraction?
219222

SampleApp/Sources/AccessibilityViewController.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,27 @@ final class AccessibilityViewController: UIViewController {
66

77
private let blueprintView = BlueprintView()
88

9+
private var isLongPressButtonDark: Bool = false {
10+
didSet {
11+
if oldValue != isLongPressButtonDark {
12+
update()
13+
}
14+
}
15+
}
16+
917
override func loadView() {
1018
view = blueprintView
19+
}
20+
21+
func update() {
1122
blueprintView.element = element
1223
}
1324

25+
override func viewDidLoad() {
26+
super.viewDidLoad()
27+
update()
28+
}
29+
1430
var firstTrigger = AccessibilityFocus.Trigger()
1531
var secondTrigger = AccessibilityFocus.Trigger()
1632

@@ -144,7 +160,17 @@ final class AccessibilityViewController: UIViewController {
144160
)
145161
.accessibilityShowsLargeContentViewer(display: .title("Large content item 5 display text", nil))
146162
}.accessibilityLargeContentViewerInteractionContainer()
147-
163+
Row {
164+
Label(text: "Long press large content", configure: { label in
165+
label.color = isLongPressButtonDark ? .white : .black
166+
})
167+
.inset(by: .init(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0))
168+
.box(background: isLongPressButtonDark ? .black : .lightGray)
169+
.onLongPress {
170+
self.isLongPressButtonDark.toggle()
171+
}
172+
.accessibilityShowsLargeContentViewer(display: .title("Long press large content display text", nil))
173+
}.accessibilityLargeContentViewerInteractionContainer()
148174
}
149175
.accessibilityContainer()
150176
.inset(uniform: 20)

SampleApp/Sources/LongPress.swift

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import BlueprintUI
2+
import BlueprintUICommonControls
3+
import Foundation
4+
import UIKit
5+
6+
public struct LongPress: Element {
7+
8+
public var wrappedElement: Element
9+
public var onLongPress: () -> Void
10+
11+
public init(onLongPress: @escaping () -> Void, wrapping element: Element) {
12+
wrappedElement = element
13+
self.onLongPress = onLongPress
14+
}
15+
16+
public var content: ElementContent {
17+
ElementContent(child: wrappedElement)
18+
}
19+
20+
public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? {
21+
LongPressableView.describe { config in
22+
config[\.onLongPress] = onLongPress
23+
}
24+
}
25+
}
26+
27+
extension Element {
28+
29+
/// Wraps the element and calls the provided closure when tapped.
30+
func onLongPress(_ callback: @escaping () -> Void) -> LongPress {
31+
LongPress(onLongPress: callback, wrapping: self)
32+
}
33+
}
34+
35+
// MARK: LongPressableView
36+
37+
private final class LongPressableView: UIView, UIGestureRecognizerDelegate {
38+
39+
var onLongPress: (() -> Void)? = nil
40+
let longPressRecognizer: UILongPressGestureRecognizer
41+
private static let defaultPressDuration: TimeInterval = 0.5
42+
private static let adjustedPressDuration: TimeInterval = 3.0
43+
44+
override init(frame: CGRect) {
45+
let longPressRecognizer = UILongPressGestureRecognizer()
46+
self.longPressRecognizer = longPressRecognizer
47+
48+
super.init(frame: frame)
49+
50+
longPressRecognizer.addTarget(self, action: #selector(longPressed(_:)))
51+
longPressRecognizer.delegate = self
52+
addGestureRecognizer(longPressRecognizer)
53+
54+
updateView()
55+
}
56+
57+
func updateView() {
58+
longPressRecognizer.minimumPressDuration = UILargeContentViewerInteraction.isEnabled ? Self.adjustedPressDuration : Self.defaultPressDuration
59+
}
60+
61+
func gestureRecognizer(
62+
_ gestureRecognizer: UIGestureRecognizer,
63+
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
64+
) -> Bool {
65+
(gestureRecognizer == longPressRecognizer) && (otherGestureRecognizer == ancestorLargeContentViewerInteraction?.gestureRecognizerForExclusionRelationship)
66+
}
67+
68+
var ancestorLargeContentViewerInteraction: UILargeContentViewerInteraction? {
69+
sequence(first: self, next: { $0.superview })
70+
.dropFirst()
71+
.lazy
72+
.compactMap { $0 as? Accessibility.LargeContentViewerInteractionContainerViewable }
73+
.first?
74+
.largeContentViewerInteraction
75+
}
76+
77+
required init?(coder aDecoder: NSCoder) {
78+
fatalError("init(coder:) has not been implemented")
79+
}
80+
81+
@objc private func longPressed(_ sender: UILongPressGestureRecognizer) {
82+
// This function is called multiple times during the lifecycle of a single long-press,
83+
// so we only listen for the "begin" state to avoid calling the onLongPress callback too many times
84+
guard sender.state == .began else { return }
85+
86+
onLongPress?()
87+
}
88+
}
89+

0 commit comments

Comments
 (0)