ListlessiOS/Helpers/KeyCommandBridge.swift (6351B)
1import SwiftUI
2import UIKit
3
4
5
6
7
8
9
10
11
12
13
14struct KeyCommandBridge: UIViewRepresentable {
15 let isActive: Bool
16 let onUp: () -> Void
17 let onDown: () -> Void
18 let onSpace: () -> Void
19 let onReturn: () -> Void
20 let onDelete: () -> Void
21 let onHome: () -> Void
22 let onEnd: () -> Void
23 let onPageUp: () -> Void
24 let onPageDown: () -> Void
25
26 func makeUIView(context: Context) -> KeyCaptureView {
27 let view = KeyCaptureView()
28 view.onUp = onUp
29 view.onDown = onDown
30 view.onSpace = onSpace
31 view.onReturn = onReturn
32 view.onDelete = onDelete
33 view.onHome = onHome
34 view.onEnd = onEnd
35 view.onPageUp = onPageUp
36 view.onPageDown = onPageDown
37 view.isActive = isActive
38 return view
39 }
40
41 func updateUIView(_ view: KeyCaptureView, context: Context) {
42 view.onUp = onUp
43 view.onDown = onDown
44 view.onSpace = onSpace
45 view.onReturn = onReturn
46 view.onDelete = onDelete
47 view.onHome = onHome
48 view.onEnd = onEnd
49 view.onPageUp = onPageUp
50 view.onPageDown = onPageDown
51 view.isActive = isActive
52
53 if isActive && !view.isFirstResponder {
54 DispatchQueue.main.async { [weak view] in
55 guard let view, view.isActive else { return }
56 view.becomeFirstResponder()
57 }
58 }
59 }
60
61 final class KeyCaptureView: UIView, IOSMenuActions {
62 var isActive = false
63 var onUp: (() -> Void)?
64 var onDown: (() -> Void)?
65 var onSpace: (() -> Void)?
66 var onReturn: (() -> Void)?
67 var onDelete: (() -> Void)?
68 var onHome: (() -> Void)?
69 var onEnd: (() -> Void)?
70 var onPageUp: (() -> Void)?
71 var onPageDown: (() -> Void)?
72
73 override var canBecomeFirstResponder: Bool { true }
74
75
76
77 override var keyCommands: [UIKeyCommand]? {
78 guard isActive else { return nil }
79 return [
80 UIKeyCommand.inputUpArrow,
81 UIKeyCommand.inputDownArrow,
82 " ",
83 "\r",
84 "\u{8}",
85 UIKeyCommand.inputHome,
86 UIKeyCommand.inputEnd,
87 UIKeyCommand.inputPageUp,
88 UIKeyCommand.inputPageDown,
89 ].map { input in
90 let cmd = UIKeyCommand(
91 input: input,
92 modifierFlags: [],
93 action: #selector(handleKeyCommand(_:))
94 )
95 cmd.wantsPriorityOverSystemBehavior = true
96 return cmd
97 }
98 }
99
100 @objc private func handleKeyCommand(_ sender: UIKeyCommand) {
101 switch sender.input {
102 case UIKeyCommand.inputUpArrow:
103 onUp?()
104 case UIKeyCommand.inputDownArrow:
105 onDown?()
106 case " ":
107 onSpace?()
108 case "\r":
109 onReturn?()
110 case "\u{8}":
111 onDelete?()
112 case UIKeyCommand.inputHome:
113 onHome?()
114 case UIKeyCommand.inputEnd:
115 onEnd?()
116 case UIKeyCommand.inputPageUp:
117 onPageUp?()
118 case UIKeyCommand.inputPageDown:
119 onPageDown?()
120 default:
121 break
122 }
123 }
124
125
126
127 @objc func handleNewItem() {
128 IOSMenuCoordinator.shared.newItem?()
129 }
130
131 @objc func handleDeleteItem() {
132 IOSMenuCoordinator.shared.deleteItem?()
133 }
134
135 @objc func handleMoveUp() {
136 IOSMenuCoordinator.shared.moveUp?()
137 }
138
139 @objc func handleMoveDown() {
140 IOSMenuCoordinator.shared.moveDown?()
141 }
142
143 @objc func handleMarkCompleted() {
144 IOSMenuCoordinator.shared.markCompleted?()
145 }
146
147 @objc func handleNavigatePageUp() {
148 IOSMenuCoordinator.shared.navigatePageUp?()
149 }
150
151 @objc func handleNavigatePageDown() {
152 IOSMenuCoordinator.shared.navigatePageDown?()
153 }
154
155 @objc func handleNavigateToFirst() {
156 IOSMenuCoordinator.shared.navigateToFirst?()
157 }
158
159 @objc func handleNavigateToLast() {
160 IOSMenuCoordinator.shared.navigateToLast?()
161 }
162
163
164
165 override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
166 switch action {
167 case IOSMenuSelectors.newItem:
168 return isActive
169 case IOSMenuSelectors.deleteItem:
170 return isActive && IOSMenuCoordinator.shared.canDelete
171 case IOSMenuSelectors.moveUp:
172 return isActive && IOSMenuCoordinator.shared.canMoveUp
173 case IOSMenuSelectors.moveDown:
174 return isActive && IOSMenuCoordinator.shared.canMoveDown
175 case IOSMenuSelectors.markCompleted:
176 return isActive && IOSMenuCoordinator.shared.canMarkCompleted
177 case IOSMenuSelectors.navigatePageUp,
178 IOSMenuSelectors.navigatePageDown,
179 IOSMenuSelectors.navigateToFirst,
180 IOSMenuSelectors.navigateToLast:
181 return isActive
182 default:
183 return super.canPerformAction(action, withSender: sender)
184 }
185 }
186
187 override func validate(_ command: UICommand) {
188 super.validate(command)
189 if command.action == IOSMenuSelectors.markCompleted {
190 command.title = IOSMenuCoordinator.shared.markCompletedTitle
191 }
192 }
193 }
194}