From 01a3aac91bc0deacf5f5f08c3d79c396231a1ec2 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Tue, 13 Jun 2023 17:14:57 +0200 Subject: [PATCH] Allow deletion of measurements --- TempTrack.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 4 +- .../xcschemes/xcschememanagement.plist | 2 +- TempTrack/Bluetooth/BluetoothClient.swift | 63 ++++++++++----- TempTrack/Bluetooth/DeviceInfo.swift | 4 + TempTrack/Bluetooth/DeviceManager.swift | 1 - TempTrack/Bluetooth/DeviceState.swift | 20 ++--- TempTrack/Bluetooth/DeviceWakeCause.swift | 77 +++++++++++++++++++ TempTrack/ContentView.swift | 1 - TempTrack/Extensions/Data+Extensions.swift | 18 ++--- TempTrack/Extensions/Date+Extensions.swift | 26 +++++++ TempTrack/Storage/TemperatureStorage.swift | 21 ++++- .../Temperature/TemperatureDataTransfer.swift | 4 + TempTrack/Temperature/TemperatureSensor.swift | 16 +++- TempTrack/Temperature/TemperatureValue.swift | 2 +- TempTrack/Views/DeviceInfoView.swift | 40 ++++------ TempTrack/Views/HistoryList.swift | 12 +++ 17 files changed, 239 insertions(+), 76 deletions(-) create mode 100644 TempTrack/Bluetooth/DeviceWakeCause.swift diff --git a/TempTrack.xcodeproj/project.pbxproj b/TempTrack.xcodeproj/project.pbxproj index 068de4f..ca7b645 100644 --- a/TempTrack.xcodeproj/project.pbxproj +++ b/TempTrack.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE22A31F20E00D30244 /* Int+Extensions.swift */; }; 88404DE52A31F23E00D30244 /* UInt16+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */; }; 88404DE92A31F7D500D30244 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE82A31F7D500D30244 /* Data+Extensions.swift */; }; + 88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */; }; 88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE04E2A2508E900114294 /* TempTrackApp.swift */; }; 88CDE0512A2508E900114294 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0502A2508E900114294 /* ContentView.swift */; }; 88CDE0532A2508EA00114294 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0522A2508EA00114294 /* Assets.xcassets */; }; @@ -50,6 +51,7 @@ 88404DE22A31F20E00D30244 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extensions.swift"; sourceTree = ""; }; 88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt16+Extensions.swift"; sourceTree = ""; }; 88404DE82A31F7D500D30244 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; + 88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceWakeCause.swift; sourceTree = ""; }; 88CDE04B2A2508E900114294 /* TempTrack.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TempTrack.app; sourceTree = BUILT_PRODUCTS_DIR; }; 88CDE04E2A2508E900114294 /* TempTrackApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTrackApp.swift; sourceTree = ""; }; 88CDE0502A2508E900114294 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -154,6 +156,7 @@ 88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */, 88CDE05E2A250F5200114294 /* DeviceState.swift */, 88CDE06C2A28A92000114294 /* DeviceInfo.swift */, + 88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */, ); path = Bluetooth; sourceTree = ""; @@ -283,6 +286,7 @@ E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */, 88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */, 88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */, + 88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */, 88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */, 88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */, 88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */, diff --git a/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b4fdc68..3658a74 100644 --- a/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", "state" : { - "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", - "version" : "4.1.1" + "revision" : "2bcd249b49178247e6b52bac7d67d6e338a40cee", + "version" : "4.1.0" } } ], diff --git a/TempTrack.xcodeproj/xcuserdata/imac.xcuserdatad/xcschemes/xcschememanagement.plist b/TempTrack.xcodeproj/xcuserdata/imac.xcuserdatad/xcschemes/xcschememanagement.plist index 31ba060..427efb2 100644 --- a/TempTrack.xcodeproj/xcuserdata/imac.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TempTrack.xcodeproj/xcuserdata/imac.xcuserdatad/xcschemes/xcschememanagement.plist @@ -28,7 +28,7 @@ TempTrack.xcscheme_^#shared#^_ orderHint - 1 + 0 diff --git a/TempTrack/Bluetooth/BluetoothClient.swift b/TempTrack/Bluetooth/BluetoothClient.swift index d023516..88ef494 100644 --- a/TempTrack/Bluetooth/BluetoothClient.swift +++ b/TempTrack/Bluetooth/BluetoothClient.swift @@ -24,7 +24,7 @@ final class BluetoothClient: ObservableObject { @Published private(set) var deviceState: DeviceState = .disconnected { didSet { - print("State: \(deviceState.text)") + print("State: \(deviceState)") if case .configured = deviceState { startRegularUpdates() } else { @@ -38,6 +38,11 @@ final class BluetoothClient: ObservableObject { didSet { updateDeviceTimeIfNeeded() collectRecordedData() + if let deviceInfo, let runningTransfer { + runningTransfer.update(info: deviceInfo) + let next = runningTransfer.nextRequest() + addRequest(next) + } } } @@ -95,7 +100,6 @@ final class BluetoothClient: ObservableObject { return } let next = openRequests.removeFirst() - //print("Starting request \(next)") guard connection.send(next.serialized) else { print("Failed to start request \(next)") @@ -106,9 +110,19 @@ final class BluetoothClient: ObservableObject { } func addRequest(_ request: BluetoothRequest) { - // TODO: Check if request already exists + defer { + performNextRequest() + } + let type = request.byte + if let runningRequest, runningRequest.byte == type { + print("Skipping duplicate request \(request)") + return + } + guard !openRequests.contains(where: { $0.byte == type }) else { + print("Skipping duplicate request \(request)") + return + } openRequests.append(request) - performNextRequest() } // MARK: Device time @@ -134,12 +148,15 @@ final class BluetoothClient: ObservableObject { @discardableResult func collectRecordedData() -> Bool { guard runningTransfer == nil else { + print("Transfer already running") return false } guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else { + print("Transfer already in scheduled") return false } guard let info = deviceInfo else { + print("No device info to start transfer") return false } guard info.numberOfStoredMeasurements > 0 else { @@ -149,38 +166,27 @@ final class BluetoothClient: ObservableObject { let transfer = TemperatureDataTransfer(info: info) runningTransfer = transfer let next = transfer.nextRequest() + print("Starting transfer") addRequest(next) return true } private func didReceive(data: Data, offset: Int, count: Int) { guard let runningTransfer else { + print("No running transfer to process device data") return // TODO: Start new transfer? } runningTransfer.add(data: data, offset: offset, count: count) - continueTransfer() - } - - private func continueTransfer() { - guard let runningTransfer else { - return // TODO: Start new transfer? - } let next = runningTransfer.nextRequest() addRequest(next) } private func decode(info: Data) { guard let newInfo = try? DeviceInfo(info: info) else { + print("Failed to decode device info") return } self.deviceInfo = newInfo - if let runningTransfer { - runningTransfer.update(info: newInfo) - let next = runningTransfer.nextRequest() - addRequest(next) - } else if newInfo.numberOfStoredMeasurements > 0 { - collectRecordedData() - } } } @@ -209,15 +215,31 @@ extension BluetoothClient: DeviceManagerDelegate { case .success: break case .responseInProgress: + print("Device is busy for \(runningRequest)") // Retry the request addRequest(runningRequest) return + case .invalidNumberOfBytesToDelete: + guard case .clearRecordingBuffer = runningRequest else { + // If clearing the recording buffer fails due to byte mismatch, + // then requesting new info will resolve the mismatch, and the transfer will be resumed + addRequest(.getInfo) + return + } + print("Request \(runningRequest) received non-matching responde about number of bytes to delete") + case .responseTooLarge: + guard case .getRecordingData = runningRequest else { + // If requesting bytes fails due to the response size, + // then requesting new info will update the response size, and the transfer will be resumed + addRequest(.getInfo) + return + } + print("Unexpectedly exceeded payload size for request \(runningRequest)") default: print("Unknown response \(data[0]) for request \(runningRequest)") // If clearing the recording buffer fails due to byte mismatch, // then requesting new info will resolve the mismatch, and the transfer will be resumed - // If requesting bytes fails due to the response size, - // then requesting new info will update the response size, and the transfer will be resumed + addRequest(.getInfo) return @@ -240,6 +262,7 @@ extension BluetoothClient: DeviceManagerDelegate { private func didClearDeviceStorage() { guard let runningTransfer else { + print("No running transfer after clearing device storage") return } runningTransfer.completeTransfer() diff --git a/TempTrack/Bluetooth/DeviceInfo.swift b/TempTrack/Bluetooth/DeviceInfo.swift index a2514dd..4676adc 100644 --- a/TempTrack/Bluetooth/DeviceInfo.swift +++ b/TempTrack/Bluetooth/DeviceInfo.swift @@ -33,6 +33,8 @@ struct DeviceInfo { let hasDeviceStartTimeSet: Bool + let wakeupReason: DeviceWakeCause + // MARK: Storage let storageSize: Int @@ -74,6 +76,7 @@ extension DeviceInfo { let deviceStartTimeSeconds = try data.decodeFourByteInteger() self.sensor0 = try data.decodeSensor() self.sensor1 = try data.decodeSensor() + self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED if deviceStartTimeSeconds != 0 { self.hasDeviceStartTimeSet = true @@ -101,6 +104,7 @@ extension DeviceInfo { numberOfSecondsRunning: 20, deviceStartTime: .now.addingTimeInterval(-20755), hasDeviceStartTimeSet: true, + wakeupReason: .WAKEUP_EXT0, storageSize: 10000, transferBlockSize: 180) } diff --git a/TempTrack/Bluetooth/DeviceManager.swift b/TempTrack/Bluetooth/DeviceManager.swift index 56a9624..6c9f036 100644 --- a/TempTrack/Bluetooth/DeviceManager.swift +++ b/TempTrack/Bluetooth/DeviceManager.swift @@ -78,7 +78,6 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate { return false } device.writeValue(data, for: characteristic, type: .withResponse) - //DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { } return self.read() } diff --git a/TempTrack/Bluetooth/DeviceState.swift b/TempTrack/Bluetooth/DeviceState.swift index 86f2fad..41b9f45 100644 --- a/TempTrack/Bluetooth/DeviceState.swift +++ b/TempTrack/Bluetooth/DeviceState.swift @@ -26,27 +26,21 @@ enum DeviceState { case .bluetoothEnabled: return "Bluetooth enabled" case .scanning: - return "Scanning for devices..." + return "Scanning..." case .connecting(let device): guard let name = device.name else { - return "Connecting to device..." + return "Connecting..." } return "Connecting to \(name)..." - case .discoveringServices(let device): - guard let name = device.name else { - return "Setting up device..." - } - return "Setting up \(name)..." - case .discoveringCharacteristic(let device): - guard let name = device.name else { - return "Setting up device..." - } - return "Setting up \(name)..." + case .discoveringServices: + return "Discovering service..." + case .discoveringCharacteristic: + return "Discovering characteristic..." case .configured(let device, _): guard let name = device.name else { return "Connected" } - return "Connected to \(name)" + return name case .disconnected: return "Not connected" } diff --git a/TempTrack/Bluetooth/DeviceWakeCause.swift b/TempTrack/Bluetooth/DeviceWakeCause.swift new file mode 100644 index 0000000..992206c --- /dev/null +++ b/TempTrack/Bluetooth/DeviceWakeCause.swift @@ -0,0 +1,77 @@ +import Foundation + +enum DeviceWakeCause: UInt8 { + + /// In case of deep sleep, reset was not caused by exit from deep sleep + case WAKEUP_UNDEFINED = 0 + + /// Not a wakeup cause, used to disable all wakeup sources with esp_sleep_disable_wakeup_source + case WAKEUP_ALL = 1 + + /// Wakeup caused by external signal using RTC_IO + case WAKEUP_EXT0 = 2 + + /// Wakeup caused by external signal using RTC_CNTL + case WAKEUP_EXT1 = 3 + + /// Wakeup caused by timer + case WAKEUP_TIMER = 4 + + /// Wakeup caused by touchpad + case WAKEUP_TOUCHPAD = 5 + + /// Wakeup caused by ULP program + case WAKEUP_ULP = 6 + + /// Wakeup caused by GPIO (light sleep only on ESP32, S2 and S3) + case WAKEUP_GPIO = 7 + + /// Wakeup caused by UART (light sleep only) + case WAKEUP_UART = 8 + + /// Wakeup caused by WIFI (light sleep only) + case WAKEUP_WIFI = 9 + + /// Wakeup caused by COCPU int + case WAKEUP_COCPU = 10 + + /// Wakeup caused by COCPU crash + case WAKEUP_COCPU_TRAP_TRIG = 11 + + /// Wakeup caused by BT (light sleep only) + case WAKEUP_BT = 12 +} + +extension DeviceWakeCause { + + var text: String { + switch self { + case .WAKEUP_UNDEFINED: + return "Power On" + case .WAKEUP_ALL: + return "" + case .WAKEUP_EXT0: + return "Button" + case .WAKEUP_EXT1: + return "EXT1" + case .WAKEUP_TIMER: + return "Timer" + case .WAKEUP_TOUCHPAD: + return "Touch" + case .WAKEUP_ULP: + return "ELP" + case .WAKEUP_GPIO: + return "GPIO" + case .WAKEUP_UART: + return "UART" + case .WAKEUP_WIFI: + return "WiFi" + case .WAKEUP_COCPU: + return "CoCPU Interupt" + case .WAKEUP_COCPU_TRAP_TRIG: + return "CoCPU Crash" + case .WAKEUP_BT: + return "Bluetooth" + } + } +} diff --git a/TempTrack/ContentView.swift b/TempTrack/ContentView.swift index d7a4501..4babb6c 100644 --- a/TempTrack/ContentView.swift +++ b/TempTrack/ContentView.swift @@ -126,7 +126,6 @@ struct ContentView: View { } .padding() .sheet(isPresented: $showDeviceInfo) { - //.bottomSheet(isPresented: $showDeviceInfo, height: 650) { if let info = bluetoothClient.deviceInfo { DeviceInfoView(info: info, isPresented: $showDeviceInfo) } else { diff --git a/TempTrack/Extensions/Data+Extensions.swift b/TempTrack/Extensions/Data+Extensions.swift index 691b212..b61401f 100644 --- a/TempTrack/Extensions/Data+Extensions.swift +++ b/TempTrack/Extensions/Data+Extensions.swift @@ -6,6 +6,13 @@ enum DeviceInfoError: Error { extension Data { + mutating func getByte() throws -> UInt8 { + guard count >= 1 else { + throw DeviceInfoError.missingData + } + return removeFirst() + } + mutating func decodeUInt16() throws -> UInt16 { guard count >= 2 else { throw DeviceInfoError.missingData @@ -29,15 +36,4 @@ extension Data { let byte3 = removeFirst() return (Int(byte3) << 24) | (Int(byte2) << 16) | (Int(byte1) << 8) | Int(byte0) } - - mutating func decodeSensor() throws -> TemperatureSensor? { - guard count >= 11 else { - throw DeviceInfoError.missingData - } - let address = Array(self[startIndex.. 1 else { + return "Now" + } + guard secs >= 60 else { + return "\(secs) s ago" + } + let minutes = secs / 60 + guard minutes >= 60 else { + return "\(minutes) min ago" + } + let hours = minutes / 60 + guard hours > 1 else { + return "1 hour ago" + } + guard hours >= 60 else { + return "\(hours) hours ago" + } + let days = hours / 24 + guard days > 1 else { + return "1 day ago" + } + return "\(days) days ago" + } + var timePassedText: String { let secs = secondsToNow guard secs > 1 else { diff --git a/TempTrack/Storage/TemperatureStorage.swift b/TempTrack/Storage/TemperatureStorage.swift index 3913c38..928a5f7 100644 --- a/TempTrack/Storage/TemperatureStorage.swift +++ b/TempTrack/Storage/TemperatureStorage.swift @@ -83,6 +83,10 @@ final class TemperatureStorage: ObservableObject { String(format: "%08d.bin", index) } + private func fileUrl(for dateIndex: Int) -> URL { + storageFolder.appendingPathComponent(fileName(for: dateIndex)) + } + private func fileUrl(for fileName: String) -> URL { storageFolder.appendingPathComponent(fileName) } @@ -148,6 +152,21 @@ final class TemperatureStorage: ObservableObject { updateLastMeasurements(measurements) } + func removeMeasurements(for dateIndex: Int) { + let fileUrl = fileUrl(for: dateIndex) + guard fm.fileExists(atPath: fileUrl.path) else { + print("No measurements for \(fileUrl.lastPathComponent)") + return + } + do { + try fm.removeItem(at: fileUrl) + dailyMeasurementCounts = dailyMeasurementCounts.filter { $0.dateIndex != dateIndex } + recentMeasurements = recentMeasurements.filter { $0.date.dateIndex != dateIndex } + } catch { + print("Failed to delete \(fileUrl.lastPathComponent): \(error)") + } + } + /** - Returns: The number of new points */ @@ -245,7 +264,7 @@ final class TemperatureStorage: ObservableObject { private extension Array where Element == TemperatureMeasurement { @discardableResult - mutating func insert(_ measurement: TemperatureMeasurement) -> Bool { + mutating func insertIntoSorted(_ measurement: TemperatureMeasurement) -> Bool { guard !contains(measurement) else { return false } diff --git a/TempTrack/Temperature/TemperatureDataTransfer.swift b/TempTrack/Temperature/TemperatureDataTransfer.swift index 3bf1588..1be3161 100644 --- a/TempTrack/Temperature/TemperatureDataTransfer.swift +++ b/TempTrack/Temperature/TemperatureDataTransfer.swift @@ -55,6 +55,10 @@ final class TemperatureDataTransfer { } func add(data: Data, offset: Int, count: Int) { + guard currentByteIndex == offset else { + print("Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)") + return + } dataBuffer.append(data) currentByteIndex += data.count processBytes() diff --git a/TempTrack/Temperature/TemperatureSensor.swift b/TempTrack/Temperature/TemperatureSensor.swift index 611f4eb..d229a85 100644 --- a/TempTrack/Temperature/TemperatureSensor.swift +++ b/TempTrack/Temperature/TemperatureSensor.swift @@ -28,7 +28,7 @@ struct TemperatureSensor { } var updateText: String { - date.timePassedText + date.shortTimePassedText } static func temperatureIcon(_ temp: Double?) -> SFSymbol { @@ -60,3 +60,17 @@ extension TemperatureSensor { self.date = Date().addingTimeInterval(-TimeInterval(secondsAgo)) } } + +extension Data { + + mutating func decodeSensor() throws -> TemperatureSensor? { + guard count >= 11 else { + throw DeviceInfoError.missingData + } + let address = Array(self[startIndex..= 60 else { - return "\(number) seconds" + return "\(number) s" } let minutes = number / 60 guard minutes > 1 else { - return "1 minute" + return "1 min" } guard minutes >= 60 else { - return "\(minutes) minutes" + return "\(minutes) min" } let hours = minutes / 60 guard hours > 1 else { @@ -49,7 +49,7 @@ struct DeviceInfoView: View { guard secs > 1 else { return "Now" } - return "In \(secs) seconds" + return "In \(secs) s" } private var storageIcon: SFSymbol { @@ -74,13 +74,7 @@ struct DeviceInfoView: View { HStack { Image(systemSymbol: sensor.temperatureIcon) .frame(width: 30) - Text(sensor.temperatureText) - } - HStack { - Image(systemSymbol: .arrowTriangle2Circlepath) - .frame(width: 30) - Text(sensor.updateText) - Spacer() + Text("\(sensor.temperatureText) (\(sensor.updateText))") } HStack { Image(systemSymbol: .tag) @@ -112,41 +106,40 @@ struct DeviceInfoView: View { guard abs(offset) > 1 else { return "No clock offset" } - return "Offset: \(offset) seconds" + return "Offset: \(offset) s" } var body: some View { NavigationView { VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) { - Text("Recording") + Text("System") .font(.headline) HStack { Image(systemSymbol: .power) .frame(width: 30) - Text(df.string(from: info.deviceStartTime)) + Text("\(df.string(from: info.deviceStartTime)) (\(runTimeString))") Spacer() } - HStack { - Image(systemSymbol: .clock) - .frame(width: 30) - Text("\(runTimeString)") - } HStack { Image(systemSymbol: .clockBadgeExclamationmark) .frame(width: 30) Text(clockOffsetText) } HStack { - Image(systemSymbol: .stopwatch) + Image(systemSymbol: .autostartstop) .frame(width: 30) - Text("Every \(info.measurementInterval) seconds") + Text("Wakeup: \(info.wakeupReason.text)") Spacer() } + } + VStack(alignment: .leading, spacing: 5) { + Text("Recording") + .font(.headline) HStack { - Image(systemSymbol: .arrowTriangle2Circlepath) + Image(systemSymbol: .stopwatch) .frame(width: 30) - Text(nextUpdateText) + Text("\(nextUpdateText) (Every \(info.measurementInterval) s)") Spacer() } } @@ -192,7 +185,6 @@ struct DeviceInfoView: View { struct DeviceInfoView_Previews: PreviewProvider { static var previews: some View { DeviceInfoView(info: .mock, isPresented: .constant(true)) - .previewLayout(.fixed(width: 375, height: 650)) } } diff --git a/TempTrack/Views/HistoryList.swift b/TempTrack/Views/HistoryList.swift index 44b3ff0..f7352e5 100644 --- a/TempTrack/Views/HistoryList.swift +++ b/TempTrack/Views/HistoryList.swift @@ -12,12 +12,24 @@ struct HistoryList: View { TemperatureDayOverview(storage: storage, dateIndex: day.dateIndex) }) { HistoryListRow(entry: day) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteRow(for: day.dateIndex) + } label: { + Label("Delete", systemSymbol: .pencil) + } + .tint(.purple) + } } } .navigationTitle("History") .navigationBarTitleDisplayMode(.large) } } + + private func deleteRow(for dateIndex: Int) { + storage.removeMeasurements(for: dateIndex) + } } struct HistoryList_Previews: PreviewProvider {