Skip to content

Commit f9fcc49

Browse files
committed
feat: key reordering
1 parent 8b6cdeb commit f9fcc49

4 files changed

Lines changed: 53 additions & 15 deletions

File tree

‎Normal/Models/Key.swift‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ final class Key: Identifiable {
1313
var longitude: Double?
1414
var radiusMeters: Double?
1515
var radiusKind: LocationRadiusKind?
16+
var sortIndex: Int = 0
1617
private(set) var hashedValue: String
1718
private(set) var salt: String
1819

1920
private static let pbkdf2Rounds: UInt32 = 100_000
2021
private static let saltLength = 16
2122
private static let hashLength = 32
2223

23-
init(name: String, type: KeyType, rawValue: String, scanKind: ScanCodeKind? = nil) {
24+
init(name: String, type: KeyType, rawValue: String, scanKind: ScanCodeKind? = nil, sortIndex: Int = 0) {
2425
id = UUID()
2526
self.name = name
2627
self.type = type
@@ -29,12 +30,20 @@ final class Key: Identifiable {
2930
longitude = nil
3031
radiusMeters = nil
3132
radiusKind = nil
33+
self.sortIndex = sortIndex
3234
let salt = Self.generateSalt()
3335
self.salt = salt
3436
hashedValue = Self.hash(unhashedString: rawValue, salt: salt)
3537
}
3638

37-
init(name: String, latitude: Double, longitude: Double, radiusMeters: Double, radiusKind: LocationRadiusKind) {
39+
init(
40+
name: String,
41+
latitude: Double,
42+
longitude: Double,
43+
radiusMeters: Double,
44+
radiusKind: LocationRadiusKind,
45+
sortIndex: Int = 0
46+
) {
3847
id = UUID()
3948
self.name = name
4049
type = .location
@@ -43,6 +52,7 @@ final class Key: Identifiable {
4352
self.longitude = longitude
4453
self.radiusMeters = radiusMeters
4554
self.radiusKind = radiusKind
55+
self.sortIndex = sortIndex
4656
salt = ""
4757
hashedValue = ""
4858
}
@@ -69,7 +79,6 @@ final class Key: Identifiable {
6979
return Self.hash(unhashedString: unhashedId, salt: salt) == hashedValue
7080
}
7181

72-
/// Whether `location` falls inside this key's saved radius.
7382
func matches(location: CLLocation) -> Bool {
7483
guard type == .location, let latitude, let longitude, let radiusMeters else { return false }
7584
let target = CLLocation(latitude: latitude, longitude: longitude)
@@ -80,13 +89,10 @@ final class Key: Identifiable {
8089
keys.filter { $0.type == .location }
8190
}
8291

83-
/// All location keys share a single kind; this returns it (or `nil` if none exist).
8492
static func existingLocationKind(in keys: [Key]) -> LocationRadiusKind? {
8593
locationKeys(in: keys).first?.radiusKind
8694
}
8795

88-
/// Verifies the location keys against the user's current position.
89-
/// Unblock radii pass when inside any zone; block radii pass when outside every zone.
9096
static func locationKeyVerifies(keys: [Key], location: CLLocation) -> Bool {
9197
let locKeys = locationKeys(in: keys)
9298
guard let kind = locKeys.first?.radiusKind else { return false }

‎Normal/Views/Key/KeyFormSheet.swift‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,18 +213,26 @@ struct KeyFormSheet: View {
213213
if let existing {
214214
existing.name = trimmed
215215
} else {
216+
let nextIndex = SortIndexing.nextIndex(after: keys, sortIndex: \.sortIndex)
216217
switch keyType {
217218
case .nfc, .qr:
218219
guard let rawValue = scannedKeyId else { return }
219-
modelContext.insert(Key(name: trimmed, type: keyType, rawValue: rawValue, scanKind: scannedKind))
220+
modelContext.insert(Key(
221+
name: trimmed,
222+
type: keyType,
223+
rawValue: rawValue,
224+
scanKind: scannedKind,
225+
sortIndex: nextIndex
226+
))
220227
case .location:
221228
guard let captured = capturedLocation else { return }
222229
modelContext.insert(Key(
223230
name: trimmed,
224231
latitude: captured.latitude,
225232
longitude: captured.longitude,
226233
radiusMeters: captured.radiusMeters,
227-
radiusKind: captured.kind
234+
radiusKind: captured.kind,
235+
sortIndex: nextIndex
228236
))
229237
}
230238
}

‎Normal/Views/Key/KeysView.swift‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import SwiftUI
44
struct KeysView: View {
55
@Environment(ScreenTimeService.self) private var screenTimeService
66

7-
@Query private var keys: [Key]
7+
@Query(sort: [SortDescriptor(\Key.sortIndex)])
8+
private var keys: [Key]
89
@State private var isShowingSheet = false
910

1011
private var isBlocked: Bool {
@@ -36,14 +37,18 @@ struct KeysView: View {
3637
if keys.isEmpty {
3738
KeysEmptyStateView { isShowingSheet = true }
3839
} else {
39-
ListView(items: keys) { key in
40+
ReorderableListView(items: keys, rowContent: { key in
4041
KeyListCardView(key: key)
41-
}
42-
.safeAreaInset(edge: .bottom) {
43-
if isBlocked {
44-
FooterMessage(text: "Unblock all apps to edit or delete keys.")
42+
}, onMove: move)
43+
.safeAreaInset(edge: .bottom) {
44+
if isBlocked {
45+
FooterMessage(text: "Unblock all apps to edit or delete keys.")
46+
}
4547
}
46-
}
4748
}
4849
}
50+
51+
private func move(from source: IndexSet, to destination: Int) {
52+
_ = SortIndexing.reorder(keys, from: source, to: destination, sortIndex: \.sortIndex)
53+
}
4954
}

‎NormalTests/Models/KeyMigrationTests.swift‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,25 @@ struct KeyMigrationTests {
7272
#expect(key.displayTypeLabel == "Barcode")
7373
}
7474

75+
@Test func sortIndexDefaultsAndPersistsAcrossReopen() throws {
76+
let url = storeURL()
77+
defer { removeStore(at: url) }
78+
79+
do {
80+
let container = try makeContainer(at: url)
81+
container.mainContext.insert(Key(name: "Legacy", type: .qr, rawValue: "a"))
82+
container.mainContext.insert(Key(name: "Ordered", type: .nfc, rawValue: "b", sortIndex: 5))
83+
try container.mainContext.save()
84+
}
85+
86+
let reopened = try makeContainer(at: url)
87+
let byName = try reopened.mainContext.fetch(FetchDescriptor<Key>())
88+
.reduce(into: [String: Key]()) { $0[$1.name] = $1 }
89+
90+
#expect(byName["Legacy"]?.sortIndex == 0)
91+
#expect(byName["Ordered"]?.sortIndex == 5)
92+
}
93+
7594
@Test func mixedLegacyAndNewKeysCoexistAfterReopen() throws {
7695
let url = storeURL()
7796
defer { removeStore(at: url) }

0 commit comments

Comments
 (0)