-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSingleRowSortableSpreadSheetView.swift
More file actions
232 lines (166 loc) · 8 KB
/
SingleRowSortableSpreadSheetView.swift
File metadata and controls
232 lines (166 loc) · 8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//
// SingleRowSortableSpreadSheetView.swift
//
// Created by Kevin Kieffer on 8/8/19.
//
// Extends the functionality of the SpreadsheetView framework available at:
// https://github.com/kishikawakatsumi/SpreadsheetView
//
// Provides a view where the user selects one and only one entire row at once,
// and includes a sorting capability delegation when the user selects a column header.
//
// The existing UITouch is not used from the framework, but instead a gesture handler is
// installed to handle taps and long presses.
//
// The view controller must implement SpreadsheetActionsDelegate and call addTapAndLongPressGestures()
// to enable the functionality.
//
// To maintain the selected row after sorting, the delegate must maintain a unique object for each row,
// and be able to provide it and test it for equality
//
import UIKit
import SpreadsheetView
protocol SpreadsheetActionsDelegate : SpreadsheetViewDelegate {
func sortBy(column: Int) //delegate receives notification to sort by the provided column index
func didSelectRow(at row: Int) //delegate receives notification that a row other than the header was selected, row > 0
func longPressDidBegin(at row: Int) //delegate receives notification that a row started a long press,
func longPressDidEnd(at row: Int) //delegate receives notification that a row ended a long pressed
//Delegate should return a unique object associated with the row
func uniqueObject(forRow row : Int) -> Any
//Delete should determine if the two unique objects are equal
func uniqueObjectsAreEqual(_ obj1 : Any, _ obj2 : Any) -> Bool
}
extension SpreadsheetView : SpreadsheetViewDelegate {
//The delegate must be a SpreadsheetActionsDelegate or the callbacks will not work
private func getDelegate() -> SpreadsheetActionsDelegate? {
if let actionsDelegate = delegate as? SpreadsheetActionsDelegate {
return actionsDelegate
}
else {
return nil
}
}
//On row pressed, notify the delegate to sort if tap was on the header, otherwise select the row and let the delegate know
private func rowPressed(at location: CGPoint) {
if let indexPath = indexPathForItem(at: location) {
guard let delegate = getDelegate() else {
return
}
if indexPath.row == 0 { //header - sort
var unique : Any?
if let selectedRow = getSelectedRow() { //has a selected row
unique = delegate.uniqueObject(forRow: selectedRow) //get a unique object at the selected row
}
delegate.sortBy(column: indexPath.column) //tell the delegate to sort its model
reloadData() //reload the data from the model
if unique != nil { //if there was a previously selected row
let _ = selectRow(forUniqueObject: unique!) //now reselect the row with the unique object
}
}
else { //on a row, select it
selectRow(at: indexPath.row)
delegate.didSelectRow(at: indexPath.row)
}
}
}
func addTapAndLongPressGestures(withMinLongPressDuration : Double) {
//These must be set to true or selection is impossible
allowsSelection = true
allowsMultipleSelection = true
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressRecognizer.delaysTouchesBegan = false
longPressRecognizer.minimumPressDuration = withMinLongPressDuration
addGestureRecognizer(longPressRecognizer)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapRecognizer)
//Note: with both a long tap and regular tap recognizer, one or the other might be called on the single tap,
//depending on whether the OS thinks a long press is starting or a single press occurred.
//Just having the long press handler is not always enough to recognize a very short tap, but a short tap might
//also be recognized as starting a long press. So both recognizers are needed and they both call the
//rowPressed() function so we're guaranteeed to handle the row select. If its a long press, that handler will
//take care of the ending of the long press
}
@objc func handleTap(sender: UITapGestureRecognizer) {
let location = sender.location(in: self)
rowPressed(at: location)
}
//On start, handle pressing of the row and notify delegate of the long press start
//When finished, notify the delegate of the long press end
@objc func handleLongPress(sender: UILongPressGestureRecognizer) {
guard let delegate = getDelegate() else {
return
}
let location = sender.location(in: self)
if let indexPath = indexPathForItem(at: location) {
switch sender.state {
case .began:
rowPressed(at: location)
delegate.longPressDidBegin(at: indexPath.row)
case .ended:
delegate.longPressDidEnd(at: indexPath.row)
default:
break
}
}
}
//Highlight the row, on or off
func highlight(on : Bool, atRow row : Int) {
for col in 0..<numberOfColumns {
let ipath = IndexPath(row: row, column: col)
cellForItem(at: ipath)?.isHighlighted = on
}
}
//Deselect all rows, then select all columns in the row
func selectRow(at row : Int) {
deselectAll()
for col in 0..<numberOfColumns {
let ipath = IndexPath(row: row, column: col)
selectItem(at: ipath, animated: false, scrollPosition: .init())
}
}
//Select the row that contains the unique object at the specified column, if the object is nil, clear selections
//Return true if a row was selected, false if all rows are deselected
func selectRow(forUniqueObject obj : Any?) -> Bool {
guard let delegate = getDelegate() else {
return false
}
guard let obj = obj, numberOfRows > 1 else {
deselectAll() //not found
return false
}
for row in 1..<numberOfRows {
let unique = delegate.uniqueObject(forRow : row) //get the unique object for each row
if delegate.uniqueObjectsAreEqual(obj, unique) { //objects match - this is the row to select
selectRow(at: row)
return true
}
}
deselectAll() //not found
return false
}
//Deselect all columns in all rows
func deselectAll() {
selectItem(at: nil, animated: false, scrollPosition: .init())
}
func getSelectedRow() -> Int? {
if numberOfRows < 1 { //nothing could be selected
return nil
}
for row in 1..<numberOfRows {
let indexPath = IndexPath(row: row, column: 0)
if let cell = cellForItem(at: indexPath) {
if cell.isSelected {
return row
}
}
}
return nil
}
// ---- Do not allow the base class SpreadsheetView to select items - they will be done by the gesture recognizers in this extension -----
public func spreadsheetView(_ spreadsheetView: SpreadsheetView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return false
}
public func spreadsheetView(_ spreadsheetView: SpreadsheetView, shouldDeselectItemAt indexPath: IndexPath) -> Bool {
return false
}
}