Add export function
This commit is contained in:
@ -8,6 +8,8 @@ protocol BluetoothDeviceDelegate: AnyObject {
|
||||
|
||||
actor BluetoothDevice: NSObject, ObservableObject {
|
||||
|
||||
private let storage: PersistentStorage!
|
||||
|
||||
let peripheral: CBPeripheral!
|
||||
|
||||
private let characteristic: CBCharacteristic!
|
||||
@ -24,9 +26,10 @@ actor BluetoothDevice: NSObject, ObservableObject {
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
init(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
|
||||
init(storage: PersistentStorage, peripheral: CBPeripheral, characteristic: CBCharacteristic) {
|
||||
self.peripheral = peripheral
|
||||
self.characteristic = characteristic
|
||||
self.storage = storage
|
||||
super.init()
|
||||
|
||||
peripheral.delegate = self
|
||||
@ -35,6 +38,7 @@ actor BluetoothDevice: NSObject, ObservableObject {
|
||||
override init() {
|
||||
self.peripheral = nil
|
||||
self.characteristic = nil
|
||||
self.storage = nil
|
||||
super.init()
|
||||
}
|
||||
|
||||
@ -46,7 +50,6 @@ actor BluetoothDevice: NSObject, ObservableObject {
|
||||
}
|
||||
lastDeviceInfo = info
|
||||
delegate?.bluetoothDevice(didUpdate: info)
|
||||
#warning("Don't use global variable")
|
||||
storage.save(deviceInfo: info)
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObje
|
||||
|
||||
private let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
|
||||
|
||||
let storage: PersistentStorage!
|
||||
|
||||
private var manager: CBCentralManager! = nil
|
||||
|
||||
@Published
|
||||
@ -71,7 +73,8 @@ final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObje
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
init(storage: PersistentStorage) {
|
||||
self.storage = storage
|
||||
connectionState = .noDeviceFound
|
||||
super.init()
|
||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||
@ -200,7 +203,7 @@ extension BluetoothScanner: CBPeripheralDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
|
||||
configuredDevice = .init(storage: storage, peripheral: peripheral, characteristic: desiredCharacteristic)
|
||||
Task {
|
||||
await configuredDevice?.set(delegate: self)
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ struct ContentView: View {
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
||||
ContentView(scanner: .init())
|
||||
ContentView(scanner: .init(storage: storage))
|
||||
.environmentObject(storage)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import Combine
|
||||
import BinaryCodable
|
||||
import SwiftUI
|
||||
import ZIPFoundation
|
||||
|
||||
final class PersistentStorage: ObservableObject {
|
||||
|
||||
@ -353,6 +354,34 @@ final class PersistentStorage: ObservableObject {
|
||||
defer { updateTransferCount() }
|
||||
return save(data: data, date: date, in: "transfers")
|
||||
}
|
||||
|
||||
// MARK: Export
|
||||
|
||||
private var zipArchive: URL {
|
||||
FileManager.default.temporaryDirectory.appendingPathComponent("data.zip")
|
||||
}
|
||||
|
||||
func createZip() throws -> URL {
|
||||
// Define the destination zip file path
|
||||
let archive = zipArchive
|
||||
|
||||
// Create the zip file
|
||||
try FileManager.default.zipItem(
|
||||
at: PersistentStorage.documentDirectory,
|
||||
to: archive,
|
||||
shouldKeepParent: false,
|
||||
compressionMethod: .deflate)
|
||||
return archive
|
||||
}
|
||||
|
||||
func removeZipArchive() throws {
|
||||
let archive = zipArchive
|
||||
// Remove existing zip file if it exists
|
||||
if FileManager.default.fileExists(atPath: archive.path) {
|
||||
print("Removing archive file")
|
||||
try FileManager.default.removeItem(at: archive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == TemperatureMeasurement {
|
||||
|
@ -1,14 +1,25 @@
|
||||
import SwiftUI
|
||||
|
||||
let storage = PersistentStorage()
|
||||
|
||||
private let scanner = BluetoothScanner()
|
||||
|
||||
private let transfer = TransferHandler()
|
||||
|
||||
@main
|
||||
struct TempTrackApp: App {
|
||||
|
||||
|
||||
@StateObject
|
||||
private var storage: PersistentStorage
|
||||
|
||||
@StateObject
|
||||
private var scanner: BluetoothScanner
|
||||
|
||||
@StateObject
|
||||
private var transfer = TransferHandler()
|
||||
|
||||
init() {
|
||||
let storage = PersistentStorage()
|
||||
self._scanner = .init(wrappedValue: .init(storage: storage))
|
||||
self._storage = .init(wrappedValue: storage)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(scanner: scanner)
|
||||
|
81
TempTrack/Views/ExportSheet.swift
Normal file
81
TempTrack/Views/ExportSheet.swift
Normal file
@ -0,0 +1,81 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ExportSheet: View {
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State
|
||||
private var isCreatingArchive = false
|
||||
|
||||
@State var url: URL?
|
||||
|
||||
@State var error: String?
|
||||
|
||||
@EnvironmentObject
|
||||
private var storage: PersistentStorage
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Export")
|
||||
.font(.title)
|
||||
Text("Create a zip file of all stored data and share it.")
|
||||
if isCreatingArchive {
|
||||
ProgressView()
|
||||
Text("Creating archive...")
|
||||
}
|
||||
if let error {
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
Spacer()
|
||||
Button("Create archive", action: createArchive)
|
||||
.disabled(isCreatingArchive)
|
||||
.padding()
|
||||
if let url {
|
||||
ShareLink(item: url) {
|
||||
Label("Share archive", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.padding()
|
||||
Button("Delete archive", action: deleteArchive)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func createArchive() {
|
||||
guard !isCreatingArchive else {
|
||||
return
|
||||
}
|
||||
isCreatingArchive = true
|
||||
DispatchQueue.main.async {
|
||||
do {
|
||||
let url = try storage.createZip()
|
||||
DispatchQueue.main.async {
|
||||
self.url = url
|
||||
self.error = nil
|
||||
self.isCreatingArchive = false
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.error = "Failed to create archive: \(error.localizedDescription)"
|
||||
self.isCreatingArchive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteArchive() {
|
||||
do {
|
||||
try storage.removeZipArchive()
|
||||
self.error = nil
|
||||
self.url = nil
|
||||
} catch {
|
||||
self.error = "Failed to delete archive: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExportSheet(isPresented: .constant(true))
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct HistoryList: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var storage: PersistentStorage
|
||||
|
||||
|
||||
@State
|
||||
private var showExportSheet = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(storage.dailyMeasurementCounts) { day in
|
||||
@ -25,6 +29,16 @@ struct HistoryList: View {
|
||||
}
|
||||
.navigationTitle("History")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button(action: { showExportSheet = true }) {
|
||||
Label("Export", systemSymbol: .arrowUpDoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showExportSheet) {
|
||||
ExportSheet(isPresented: $showExportSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user