Skip to content

Commit

Permalink
reorder control
Browse files Browse the repository at this point in the history
  • Loading branch information
RoyalPineapple committed Mar 28, 2024
1 parent 12b5e65 commit ce3b914
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 23 deletions.
96 changes: 78 additions & 18 deletions BlueprintUILists/Sources/ListReorderGesture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Kyle Van Essen on 11/14/19.
//

import Accessibility
import BlueprintUI
import ListableUI
import UIKit
Expand Down Expand Up @@ -38,6 +39,7 @@ import UIKit
/// ```
public struct ListReorderGesture : Element
{

public enum Begins {
case onTap
case onLongPress
Expand All @@ -54,9 +56,8 @@ public struct ListReorderGesture : Element

let actions : ReorderingActions

/// The acccessibility Label of the item that will be reordered.
/// This will be set as the gesture's accessibilityValue to provide a richer VoiceOver utterance.
public var reorderItemAccessibilityLabel : String? = nil
/// The acccessibility label for the reorder element. Defaults to "Reorder".
public var accessibilityLabel : String?

/// Creates a new re-order gesture which wraps the provided element.
///
Expand All @@ -66,6 +67,7 @@ public struct ListReorderGesture : Element
isEnabled : Bool = true,
actions : ReorderingActions,
begins: Begins = .onTap,
accessibilityLabel: String? = nil,
wrapping element : Element
) {
self.isEnabled = isEnabled
Expand All @@ -74,6 +76,8 @@ public struct ListReorderGesture : Element

self.begins = begins

self.accessibilityLabel = accessibilityLabel

self.element = element
}

Expand All @@ -88,24 +92,16 @@ public struct ListReorderGesture : Element
public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription?
{
return ViewDescription(View.self) { config in

config.builder = {
View(frame: context.bounds, wrapping: self)
}
config.contentView = { $0.containerView }

config.apply { view in
view.isAccessibilityElement = true
view.accessibilityLabel = ListableLocalizedStrings.ReorderGesture.accessibilityLabel
view.accessibilityValue = reorderItemAccessibilityLabel
view.accessibilityHint = ListableLocalizedStrings.ReorderGesture.accessibilityHint
view.accessibilityTraits.formUnion(.button)
view.accessibilityCustomActions = accessibilityActions()

view.recognizer.isEnabled = self.isEnabled

view.recognizer.apply(actions: self.actions)

view.recognizer.minimumPressDuration = begins == .onLongPress ? 0.5 : 0.0
view.apply(self)
}

}
}

Expand All @@ -118,9 +114,14 @@ public extension Element
func listReorderGesture(
with actions : ReorderingActions,
isEnabled : Bool = true,
begins: ListReorderGesture.Begins = .onTap
begins: ListReorderGesture.Begins = .onTap,
accessibilityLabel: String? = nil
) -> Element {
ListReorderGesture(isEnabled: isEnabled, actions: actions, begins: begins, wrapping: self)
ListReorderGesture(isEnabled: isEnabled,
actions: actions,
begins: begins,
accessibilityLabel: accessibilityLabel,
wrapping: self)
}
}

Expand All @@ -129,25 +130,84 @@ fileprivate extension ListReorderGesture
{
private final class View : UIView
{

let containerView = UIView()
let recognizer : ItemReordering.GestureRecognizer
private lazy var proxyElement = UIAccessibilityElement(accessibilityContainer: self)
private var minimumPressDuration: TimeInterval = 0.0 {
didSet {
updateGesturePressDuration()
}
}

@objc private func updateGesturePressDuration() {
self.recognizer.minimumPressDuration = UIAccessibility.isVoiceOverRunning ? 0.0 : self.minimumPressDuration
}

init(frame: CGRect, wrapping : ListReorderGesture)
{
self.recognizer = .init()

super.init(frame: frame)

recognizer.accessibilityProxy = proxyElement
NotificationCenter.default.addObserver(self, selector: #selector(updateGesturePressDuration) , name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)

self.isOpaque = false
self.clipsToBounds = false
self.backgroundColor = .clear

self.addGestureRecognizer(self.recognizer)

self.isAccessibilityElement = false

containerView.isOpaque = false
containerView.backgroundColor = .clear
addSubview(containerView)
}

@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
listableInternalFatal()
}

func apply(_ model: ListReorderGesture) {
proxyElement.accessibilityLabel = model.accessibilityLabel ?? ListableLocalizedStrings.ReorderGesture.accessibilityLabel
proxyElement.accessibilityHint = ListableLocalizedStrings.ReorderGesture.accessibilityHint
proxyElement.accessibilityTraits.formUnion(.button)
proxyElement.accessibilityCustomActions = model.accessibilityActions()

recognizer.isEnabled = model.isEnabled

recognizer.apply(actions: model.actions)
minimumPressDuration = model.begins == .onLongPress ? 0.5 : 0.0
}

override func layoutSubviews() {
super.layoutSubviews()
containerView.frame = bounds
}

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if UIAccessibility.isVoiceOverRunning,
UIAccessibility.focusedElement(using: .notificationVoiceOver) as? NSObject == proxyElement {
// Intercept touch events to avoid activating contained elements.
return self
}

return super.hitTest(point, with: event)
}

override var accessibilityElements: [Any]? {
get {
guard recognizer.isEnabled else { return super.accessibilityElements }
proxyElement.accessibilityFrame = self.accessibilityFrame
proxyElement.accessibilityActivationPoint = self.accessibilityActivationPoint
return [containerView, proxyElement]
}
set {
fatalError("Cannot set accessibility elements directly")
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# [Main]

### Fixed
- `ListReorderGesture` no longer blocks child accessibility, now exposing a proxy element for accessible control.

### Added

Expand Down
68 changes: 64 additions & 4 deletions Demo/Sources/Demos/Demo Screens/CollectionViewAppearance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,65 @@ struct DemoHeader2 : BlueprintHeaderFooterContent, Equatable
}


struct DemoTile : BlueprintItemContent, Equatable, LocalizedCollatableItemContent
{
var text : String
var secondaryText: String

var identifierValue: String {
return "\(text) \(secondaryText)"
}

func element(with info : ApplyItemContentInfo) -> Element
{
Button(onTap:{
print("\(text) tapped!")
}, wrapping: Row { row in
row.verticalAlignment = .center

row.add(child:
Column { col in
col.add(child: Label(text: text) {
$0.font = .systemFont(ofSize: 17.0, weight: .medium)
$0.color = info.state.isActive ? .white : .darkGray
})
col.add(child: Label(text: secondaryText) {
$0.font = .systemFont(ofSize: 12.0, weight: .light)
$0.color = info.state.isActive ? .white : .gray
})
}
.inset(horizontal: 15.0, vertical: 24.0)
)
})
.accessibilityElement(label: text, value: secondaryText, traits: [.button])
.listReorderGesture(with: info.reorderingActions, begins: .onLongPress, reorderItemAccessibilityLabel: text)


}

func backgroundElement(with info: ApplyItemContentInfo) -> Element?
{
Box(
backgroundColor: info.state.isReordering ? .white(0.8) : .white,
cornerStyle: .rounded(radius: 8.0)
)
}

func selectedBackgroundElement(with info: ApplyItemContentInfo) -> Element?
{
Box(
backgroundColor: .white(0.2),
cornerStyle: .rounded(radius: 8.0),
shadowStyle: .simple(radius: 2.0, opacity: 0.15, offset: .init(width: 0.0, height: 1.0), color: .black)
)
}

var collationString: String {
return "\(text) \(secondaryText)"
}
}


struct DemoItem : BlueprintItemContent, Equatable, LocalizedCollatableItemContent
{
var text : String
Expand All @@ -142,10 +201,11 @@ struct DemoItem : BlueprintItemContent, Equatable, LocalizedCollatableItemConten

if info.isReorderable {
row.addFixed(
child: Image(
image: UIImage(named: "ReorderControl"),
contentMode: .center
)
child:
Image(
image: UIImage(named: "ReorderControl"),
contentMode: .center
)
.listReorderGesture(with: info.reorderingActions, begins: requiresLongPress ? .onLongPress : .onTap)
)
}
Expand Down
18 changes: 18 additions & 0 deletions Demo/Sources/Demos/Demo Screens/ReorderingViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,23 @@ final class ReorderingViewController : ListViewController
item.reordering = ItemReordering(sections: .current)
}
}

list += Section("5") { section in
section.header = DemoHeader(title: "Tile Section")
section.layouts.table.columns = .init(count: 2, spacing: 15.0)

section += Item(DemoTile(text: "Item 0", secondaryText: "Section 4")) { item in
item.reordering = ItemReordering(sections: .current)
}
section += Item(DemoTile(text: "Item 1", secondaryText: "Section 4")) { item in
item.reordering = ItemReordering(sections: .current)
}
section += Item(DemoTile(text: "Item 2", secondaryText: "Section 4")) { item in
item.reordering = ItemReordering(sections: .current)
}
section += Item(DemoTile(text: "Item 3", secondaryText: "Section 4")) { item in
item.reordering = ItemReordering(sections: .current)
}
}
}
}
15 changes: 15 additions & 0 deletions ListableUI/Sources/Item/ItemReordering.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,16 @@ extension ItemReordering {
private var onMove : OnMove? = nil
private var onEnd : OnEnd? = nil

// If this is set the gesture recognizer will only fire when the accessibilityProxy is selected by voiceover.
public var accessibilityProxy: NSObject?

/// Creates a gesture recognizer with the provided target and selector.
public override init(target: Any?, action: Selector?)
{
super.init(target: target, action: action)

self.addTarget(self, action: #selector(updated))

self.minimumPressDuration = 0
}

Expand Down Expand Up @@ -206,6 +210,10 @@ extension ItemReordering {

@objc private func updated()
{
guard accessibilityShouldContinue() else {
self.state = .cancelled
return
}
switch self.state {
case .possible: break
case .began:
Expand All @@ -228,6 +236,13 @@ extension ItemReordering {
@unknown default: listableInternalFatal()
}
}

private func accessibilityShouldContinue() -> Bool {
guard UIAccessibility.isVoiceOverRunning, let proxy = accessibilityProxy else {
return true
}
return UIAccessibility.focusedElement(using: .notificationVoiceOver) as? NSObject == proxy
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion ListableUI/Sources/ListableLocalizedStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct ListableLocalizedStrings {
bundle: .listableUIResources,
value: "Reorder",
comment: "Accessibility label for the reorder control on an item")

public static let accessibilityHint = NSLocalizedString("reorder.AccessibilityHint",
tableName: nil,
bundle: .listableUIResources,
Expand Down

0 comments on commit ce3b914

Please sign in to comment.