Prettify main view, add temperature history
This commit is contained in:
parent
6e0910e47f
commit
002eb11dc1
@ -26,6 +26,9 @@
|
|||||||
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */; };
|
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */; };
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */; };
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */; };
|
||||||
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
||||||
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */; };
|
||||||
|
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */; };
|
||||||
|
E253A9272A2CA48A00EC6B28 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = E253A9262A2CA48A00EC6B28 /* SQLite */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -47,6 +50,8 @@
|
|||||||
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureSensor.swift; sourceTree = "<group>"; };
|
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureSensor.swift; sourceTree = "<group>"; };
|
||||||
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequest.swift; sourceTree = "<group>"; };
|
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequest.swift; sourceTree = "<group>"; };
|
||||||
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
||||||
|
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureHistoryChart.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -54,6 +59,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E253A9272A2CA48A00EC6B28 /* SQLite in Frameworks */,
|
||||||
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */,
|
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */,
|
||||||
88CDE06B2A2899C900114294 /* BottomSheet in Frameworks */,
|
88CDE06B2A2899C900114294 /* BottomSheet in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -81,14 +87,12 @@
|
|||||||
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
||||||
|
88CDE0502A2508E900114294 /* ContentView.swift */,
|
||||||
|
E253A9202A2B39A700EC6B28 /* Extensions */,
|
||||||
88CDE07C2A28AFE700114294 /* Views */,
|
88CDE07C2A28AFE700114294 /* Views */,
|
||||||
88CDE0792A28AF3E00114294 /* Bluetooth */,
|
88CDE0792A28AF3E00114294 /* Bluetooth */,
|
||||||
88CDE06E2A28AE8D00114294 /* Temperature */,
|
88CDE06E2A28AE8D00114294 /* Temperature */,
|
||||||
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
|
||||||
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
|
||||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
|
||||||
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
|
||||||
88CDE0502A2508E900114294 /* ContentView.swift */,
|
|
||||||
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
||||||
88CDE0542A2508EA00114294 /* Preview Content */,
|
88CDE0542A2508EA00114294 /* Preview Content */,
|
||||||
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
||||||
@ -122,6 +126,9 @@
|
|||||||
children = (
|
children = (
|
||||||
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
||||||
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
||||||
|
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
||||||
|
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
||||||
|
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
||||||
);
|
);
|
||||||
path = Bluetooth;
|
path = Bluetooth;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -130,10 +137,19 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */,
|
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */,
|
||||||
|
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E253A9202A2B39A700EC6B28 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -153,6 +169,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
88CDE0652A25D08F00114294 /* SFSafeSymbols */,
|
88CDE0652A25D08F00114294 /* SFSafeSymbols */,
|
||||||
88CDE06A2A2899C900114294 /* BottomSheet */,
|
88CDE06A2A2899C900114294 /* BottomSheet */,
|
||||||
|
E253A9262A2CA48A00EC6B28 /* SQLite */,
|
||||||
);
|
);
|
||||||
productName = TempTrack;
|
productName = TempTrack;
|
||||||
productReference = 88CDE04B2A2508E900114294 /* TempTrack.app */;
|
productReference = 88CDE04B2A2508E900114294 /* TempTrack.app */;
|
||||||
@ -185,6 +202,7 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
88CDE0642A25D08F00114294 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
88CDE0642A25D08F00114294 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */,
|
88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */,
|
||||||
|
E253A9252A2CA48900EC6B28 /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 88CDE04C2A2508E900114294 /* Products */;
|
productRefGroup = 88CDE04C2A2508E900114294 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -223,6 +241,8 @@
|
|||||||
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
||||||
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
||||||
|
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */,
|
||||||
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
||||||
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
||||||
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
||||||
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
||||||
@ -449,6 +469,14 @@
|
|||||||
minimumVersion = 1.0.0;
|
minimumVersion = 1.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
E253A9252A2CA48900EC6B28 /* XCRemoteSwiftPackageReference "SQLite" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 0.14.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@ -462,6 +490,11 @@
|
|||||||
package = 88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */;
|
package = 88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */;
|
||||||
productName = BottomSheet;
|
productName = BottomSheet;
|
||||||
};
|
};
|
||||||
|
E253A9262A2CA48A00EC6B28 /* SQLite */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E253A9252A2CA48900EC6B28 /* XCRemoteSwiftPackageReference "SQLite" */;
|
||||||
|
productName = SQLite;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 88CDE0432A2508E800114294 /* Project object */;
|
rootObject = 88CDE0432A2508E800114294 /* Project object */;
|
||||||
|
@ -14,8 +14,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
"revision" : "2bcd249b49178247e6b52bac7d67d6e338a40cee",
|
||||||
"version" : "4.1.1"
|
"version" : "4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sqlite.swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/stephencelis/SQLite.swift",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
|
||||||
|
"version" : "0.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
BIN
TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
BIN
TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
Binary file not shown.
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>SchemeUserState</key>
|
||||||
|
<dict>
|
||||||
|
<key>SQLite (Playground) 1.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
|
<key>SQLite (Playground) 2.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
|
<key>SQLite (Playground).xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>TempTrack.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -55,7 +55,9 @@ final class BluetoothClient: ObservableObject {
|
|||||||
private var runningTransfer: TemperatureDataTransfer?
|
private var runningTransfer: TemperatureDataTransfer?
|
||||||
|
|
||||||
func updateDeviceInfo() {
|
func updateDeviceInfo() {
|
||||||
addRequest(.getInfo)
|
if case .configured = deviceState {
|
||||||
|
addRequest(.getInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var dataUpdateTimer: Timer?
|
private var dataUpdateTimer: Timer?
|
||||||
|
@ -3,20 +3,56 @@ import SFSafeSymbols
|
|||||||
import BottomSheet
|
import BottomSheet
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
|
private let updateInterval = 1.0
|
||||||
|
|
||||||
|
private let minTempColor = Color(hue: 0.624, saturation: 0.5, brightness: 1.0)
|
||||||
|
private let minTemperature = -20.0
|
||||||
|
|
||||||
|
private let maxTempColor = Color(hue: 1.0, saturation: 0.5, brightness: 1.0)
|
||||||
|
private let maxTemperature = 40.0
|
||||||
|
|
||||||
|
private let disconnectedColor = Color(white: 0.8)
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var client = BluetoothClient()
|
var client = BluetoothClient()
|
||||||
|
|
||||||
init() {
|
@ObservedObject
|
||||||
|
var storage = TemperatureStorage()
|
||||||
}
|
|
||||||
|
|
||||||
init(client: BluetoothClient) {
|
|
||||||
self.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
var showDeviceInfo = false
|
var showDeviceInfo = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var updateTimer: Timer?
|
||||||
|
|
||||||
|
@State
|
||||||
|
var updateInfoToggle = true
|
||||||
|
|
||||||
|
init() {
|
||||||
|
startRegularUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(client: BluetoothClient, values: [TemperatureMeasurement]) {
|
||||||
|
self.client = client
|
||||||
|
self.storage = .init(lastMeasurements: values)
|
||||||
|
startRegularUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startRegularUpdates() {
|
||||||
|
guard updateTimer == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { timer in
|
||||||
|
self.updateInfoToggle.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimer?.fire()
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasDeviceInfo: Bool {
|
||||||
|
client.deviceInfo != nil
|
||||||
|
}
|
||||||
|
|
||||||
var averageTemperature: Double? {
|
var averageTemperature: Double? {
|
||||||
let t1 = client.deviceInfo?.sensor1?.optionalValue
|
let t1 = client.deviceInfo?.sensor1?.optionalValue
|
||||||
@ -54,54 +90,77 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
return .thermometerHigh
|
return .thermometerHigh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var backgroundColor: Color {
|
||||||
|
guard let temp = averageTemperature else {
|
||||||
|
return disconnectedColor
|
||||||
|
}
|
||||||
|
guard temp > minTemperature else {
|
||||||
|
return minTempColor
|
||||||
|
}
|
||||||
|
guard temp < maxTemperature else {
|
||||||
|
return maxTempColor
|
||||||
|
}
|
||||||
|
let ratio = (temp - minTemperature) / (maxTemperature - minTemperature)
|
||||||
|
return minTempColor.blend(to: maxTempColor, intensity: ratio)
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundGradient: Gradient {
|
||||||
|
let color = backgroundColor
|
||||||
|
let lighter = color.opacity(0.5)
|
||||||
|
return .init(colors: [lighter, color])
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
|
||||||
Image(systemSymbol: .iphone)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text(client.deviceState.text)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(systemSymbol: temperatureIcon)
|
// Image(systemSymbol: temperatureIcon)
|
||||||
.font(.system(size: 200, weight: .light))
|
// .font(.system(size: 100, weight: .light))
|
||||||
if hasTemperature {
|
if hasTemperature {
|
||||||
Text(temperatureString)
|
Text(temperatureString)
|
||||||
.font(.system(size: 100, weight: .light))
|
.font(.system(size: 150, weight: .light))
|
||||||
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
TemperatureHistoryChart(points: storage.lastMeasurements)
|
||||||
|
.frame(height: 150)
|
||||||
|
.background(Color.white.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Button(action: {
|
|
||||||
_ = client.collectRecordedData()
|
|
||||||
}) {
|
|
||||||
Text("Transfer")
|
|
||||||
}.padding()
|
|
||||||
Spacer()
|
|
||||||
Button {
|
Button {
|
||||||
self.showDeviceInfo = true
|
self.showDeviceInfo = true
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemSymbol: .infoCircle)
|
if hasDeviceInfo {
|
||||||
.font(.system(size: 40, weight: .regular))
|
Image(systemSymbol: .iphone)
|
||||||
}.disabled(client.deviceInfo == nil)
|
.font(.system(size: 30, weight: .regular))
|
||||||
|
}
|
||||||
|
Text(client.deviceState.text)
|
||||||
|
}
|
||||||
|
.disabled(!hasDeviceInfo)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.bottomSheet(isPresented: $showDeviceInfo, height: 520) {
|
.bottomSheet(isPresented: $showDeviceInfo, height: 600) {
|
||||||
if let info = client.deviceInfo {
|
if let info = client.deviceInfo {
|
||||||
DeviceInfoView(info: info)
|
DeviceInfoView(
|
||||||
|
info: info,
|
||||||
|
isPresented: $showDeviceInfo, updateToggle: $updateInfoToggle)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.background(backgroundGradient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView(client: BluetoothClient(deviceInfo: .mock))
|
ContentView(
|
||||||
|
client: BluetoothClient(deviceInfo: .mock),
|
||||||
|
values: TemperatureMeasurement.mockData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
TempTrack/Extensions/Color+Extensions.swift
Normal file
24
TempTrack/Extensions/Color+Extensions.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
|
||||||
|
func blend(to other: Color, intensity: CGFloat = 0.5) -> Color {
|
||||||
|
Color(UIColor(self).blend(to: UIColor(other), intensity: intensity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIColor {
|
||||||
|
|
||||||
|
func blend(to other: UIColor, intensity: CGFloat = 0.5) -> UIColor {
|
||||||
|
let l2 = max(0.0, min(1.0, intensity))
|
||||||
|
let l1 = 1 - l2
|
||||||
|
var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
||||||
|
var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
||||||
|
|
||||||
|
getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
|
||||||
|
other.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
|
||||||
|
|
||||||
|
return UIColor(red: l1*r1 + l2*r2, green: l1*g1 + l2*g2, blue: l1*b1 + l2*b2, alpha: l1*a1 + l2*a2)
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,3 @@
|
|||||||
//
|
|
||||||
// TempTrackApp.swift
|
|
||||||
// TempTrack
|
|
||||||
//
|
|
||||||
// Created by iMac on 29.05.23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
|
@ -1,10 +1,113 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TemperatureMeasurement {
|
struct TemperatureMeasurement: Identifiable {
|
||||||
|
|
||||||
var sensor0: TemperatureValue
|
var sensor0: TemperatureValue
|
||||||
|
|
||||||
var sensor1: TemperatureValue
|
var sensor1: TemperatureValue
|
||||||
|
|
||||||
var date: Date
|
var date: Date
|
||||||
|
|
||||||
|
var id: Int {
|
||||||
|
Int(date.timeIntervalSince1970.rounded())
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondsAgo: Int {
|
||||||
|
Int(date.timeIntervalSinceNow.rounded())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TemperatureValue {
|
||||||
|
|
||||||
|
init(value: Double?) {
|
||||||
|
if let value {
|
||||||
|
self = .value(value)
|
||||||
|
} else {
|
||||||
|
self = .notFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TemperatureMeasurement {
|
||||||
|
|
||||||
|
init(t0: Double?, t1: Double?, secs: Int) {
|
||||||
|
self.sensor0 = .init(value: t0)
|
||||||
|
self.sensor1 = .init(value: t1)
|
||||||
|
self.date = Date().addingTimeInterval(TimeInterval(secs-3600))
|
||||||
|
}
|
||||||
|
|
||||||
|
init(t0: Double?, t1: Double?, min: Int) {
|
||||||
|
self.init(t0: t0, t1: t1, secs: min * 60)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TemperatureMeasurement {
|
||||||
|
|
||||||
|
static let mockData: [TemperatureMeasurement] = {
|
||||||
|
let temps: [(Double?, Double?)] = [
|
||||||
|
(20, 14),
|
||||||
|
(20, 13.5),
|
||||||
|
(20.5, 13.5),
|
||||||
|
(20.5, 13.5),
|
||||||
|
(21, 14),
|
||||||
|
(21, 14),
|
||||||
|
(nil, 14.5),
|
||||||
|
(nil, 14),
|
||||||
|
(nil, 14.5),
|
||||||
|
(nil, 14),
|
||||||
|
(nil, 14),
|
||||||
|
(nil, 14.5),
|
||||||
|
(nil, 15),
|
||||||
|
(5.0, 15),
|
||||||
|
(4.5, 15.5),
|
||||||
|
(4.5, 16),
|
||||||
|
(4.0, 16.5),
|
||||||
|
(3.0, 17),
|
||||||
|
(3.0, 19),
|
||||||
|
(2.5, 20),
|
||||||
|
(2.5, 20.5),
|
||||||
|
(2.0, 20.5),
|
||||||
|
(1.0, 20.5),
|
||||||
|
(0.5, 20.5),
|
||||||
|
(0.0, 20),
|
||||||
|
(0.0, 20),
|
||||||
|
(-1.0, 21.0),
|
||||||
|
(-0.5, 21.0),
|
||||||
|
(-3.0, 21.0),
|
||||||
|
(-3.5, 20.5),
|
||||||
|
(-4.0, 20.5),
|
||||||
|
(-5.0, 20.0),
|
||||||
|
(-5.0, nil),
|
||||||
|
(-5.5, nil),
|
||||||
|
(-5.0, nil),
|
||||||
|
(-5.5, nil),
|
||||||
|
(-6.0, nil),
|
||||||
|
(-5.0, nil),
|
||||||
|
(nil, nil),
|
||||||
|
(nil, nil),
|
||||||
|
(nil, nil),
|
||||||
|
(-5.0, nil),
|
||||||
|
(-4.5, nil),
|
||||||
|
(-4.0, 23.0),
|
||||||
|
(5.0, 24.0),
|
||||||
|
(7.0, 25.0),
|
||||||
|
(8.0, 25.5),
|
||||||
|
(8.5, 25.5),
|
||||||
|
(10.0, 25.5),
|
||||||
|
(10.5, 24.0),
|
||||||
|
(10.5, 24.0),
|
||||||
|
(10.5, 24.5),
|
||||||
|
(12.0, 23.5),
|
||||||
|
(12.5, 24.0),
|
||||||
|
(12.0, 23.5),
|
||||||
|
(14.0, 24.0),
|
||||||
|
(15.0, 25.0),
|
||||||
|
(15.0, 25.0),
|
||||||
|
(15.5, 25.0),
|
||||||
|
(15.0, 25.0),
|
||||||
|
]
|
||||||
|
return temps.enumerated().map {
|
||||||
|
TemperatureMeasurement(t0: $0.element.0, t1: $0.element.1, min: $0.offset)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ enum TemperatureValue {
|
|||||||
return "No sensor"
|
return "No sensor"
|
||||||
case .invalidMeasurement:
|
case .invalidMeasurement:
|
||||||
return "Invalid"
|
return "Invalid"
|
||||||
case .value(let double):
|
case .value(let value):
|
||||||
return "\(Int(double.rounded()))°C"
|
return String(format:" %.1f°C", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,70 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import SQLite
|
||||||
|
|
||||||
final class TemperatureStorage {
|
final class TemperatureStorage: ObservableObject {
|
||||||
|
|
||||||
|
static var documentDirectory: URL {
|
||||||
|
try! FileManager.default.url(
|
||||||
|
for: .documentDirectory,
|
||||||
|
in: .userDomainMask,
|
||||||
|
appropriateFor: nil, create: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let databaseUrl: URL
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var lastMeasurements: [TemperatureMeasurement]
|
||||||
|
|
||||||
|
init(lastMeasurements: [TemperatureMeasurement] = []) {
|
||||||
|
self.lastMeasurements = lastMeasurements
|
||||||
|
self.databaseUrl = TemperatureStorage.documentDirectory.appendingPathComponent("db.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
private let table = Table("values")
|
||||||
|
private let i
|
||||||
|
|
||||||
|
private func createDatabaseIfNeeded() throws {
|
||||||
|
let db = try Connection(databaseUrl.path)
|
||||||
|
|
||||||
|
let users = Table("users")
|
||||||
|
let id = Expression<Int64>("id")
|
||||||
|
let name = Expression<String?>("name")
|
||||||
|
let email = Expression<String>("email")
|
||||||
|
|
||||||
|
try db.run(users.create(ifNotExists: true) { t in
|
||||||
|
t.column(id, primaryKey: true)
|
||||||
|
t.column(name)
|
||||||
|
t.column(email, unique: true)
|
||||||
|
})
|
||||||
|
// CREATE TABLE "users" (
|
||||||
|
// "id" INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
// "name" TEXT,
|
||||||
|
// "email" TEXT NOT NULL UNIQUE
|
||||||
|
// )
|
||||||
|
|
||||||
|
let insert = users.insert(name <- "Alice", email <- "alice@mac.com")
|
||||||
|
let rowid = try db.run(insert)
|
||||||
|
// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com')
|
||||||
|
|
||||||
|
for user in try db.prepare(users) {
|
||||||
|
print("id: \(user[id]), name: \(user[name]), email: \(user[email])")
|
||||||
|
// id: 1, name: Optional("Alice"), email: alice@mac.com
|
||||||
|
}
|
||||||
|
// SELECT * FROM "users"
|
||||||
|
|
||||||
|
let alice = users.filter(id == rowid)
|
||||||
|
|
||||||
|
try db.run(alice.update(email <- email.replace("mac.com", with: "me.com")))
|
||||||
|
// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com')
|
||||||
|
// WHERE ("id" = 1)
|
||||||
|
|
||||||
|
try db.run(alice.delete())
|
||||||
|
// DELETE FROM "users" WHERE ("id" = 1)
|
||||||
|
|
||||||
|
try db.scalar(users.count) // 0
|
||||||
|
// SELECT count(*) FROM "users"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,12 @@ struct DeviceInfoView: View {
|
|||||||
private let storageWarnBytes = 500
|
private let storageWarnBytes = 500
|
||||||
|
|
||||||
let info: DeviceInfo
|
let info: DeviceInfo
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var isPresented: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var updateToggle: Bool
|
||||||
|
|
||||||
private var runTimeString: String {
|
private var runTimeString: String {
|
||||||
let number = info.numberOfSecondsRunning
|
let number = info.numberOfSecondsRunning
|
||||||
@ -97,6 +103,16 @@ struct DeviceInfoView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
HStack {
|
||||||
|
Text("Device Info").font(.title2).bold()
|
||||||
|
Spacer()
|
||||||
|
Button(action: { isPresented = false }) {
|
||||||
|
Image(systemSymbol: .xmarkCircleFill)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.system(size: 26))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Recording")
|
Text("Recording")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@ -159,8 +175,11 @@ struct DeviceInfoView: View {
|
|||||||
|
|
||||||
struct DeviceInfoView_Previews: PreviewProvider {
|
struct DeviceInfoView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DeviceInfoView(info: .mock)
|
DeviceInfoView(
|
||||||
.previewLayout(.fixed(width: 375, height: 500))
|
info: .mock,
|
||||||
|
isPresented: .constant(true),
|
||||||
|
updateToggle: .constant(true))
|
||||||
|
.previewLayout(.fixed(width: 375, height: 600))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
TempTrack/Views/TemperatureHistoryChart.swift
Normal file
58
TempTrack/Views/TemperatureHistoryChart.swift
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Charts
|
||||||
|
|
||||||
|
struct TemperatureHistoryChart: View {
|
||||||
|
|
||||||
|
let points: [TemperatureMeasurement]
|
||||||
|
|
||||||
|
let upperTempLimit = 40.0
|
||||||
|
let lowerTempLimit = -20.0
|
||||||
|
|
||||||
|
let pastDateLimit = -3600
|
||||||
|
let futureDateLimit = 0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Chart {
|
||||||
|
ForEach(points) { point in
|
||||||
|
if let s = point.sensor0.optionalValue {
|
||||||
|
LineMark(
|
||||||
|
x: .value("Date", point.secondsAgo),
|
||||||
|
y: .value("Temperature", s))
|
||||||
|
.foregroundStyle(Color.red)
|
||||||
|
}
|
||||||
|
if let s = point.sensor1.optionalValue {
|
||||||
|
LineMark(
|
||||||
|
x: .value("Date", point.secondsAgo),
|
||||||
|
y: .value("Temperature", s))
|
||||||
|
.foregroundStyle(by: .value("Type", "Sensor 1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartXScale(domain: pastDateLimit...futureDateLimit)
|
||||||
|
.chartYScale(domain: lowerTempLimit...upperTempLimit)
|
||||||
|
.chartXAxis(.hidden)
|
||||||
|
.chartLegend(.hidden)
|
||||||
|
.chartYAxis {
|
||||||
|
AxisMarks(position: .trailing, values: .automatic) { value in
|
||||||
|
AxisValueLabel(multiLabelAlignment: .trailing) {
|
||||||
|
if let intValue = value.as(Int.self) {
|
||||||
|
Text("\(intValue) km")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//AxisMarks(position: .trailing, stroke: StrokeStyle(lineWidth: 0))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TemperatureHistoryChart_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TemperatureHistoryChart(
|
||||||
|
points: TemperatureMeasurement.mockData)
|
||||||
|
.previewLayout(.fixed(width: 350, height: 150))
|
||||||
|
.background(.gray)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user