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 */; };
|
||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07A2A28AF5100114294 /* BluetoothRequest.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 */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -47,6 +50,8 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -54,6 +59,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E253A9272A2CA48A00EC6B28 /* SQLite in Frameworks */,
|
||||
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */,
|
||||
88CDE06B2A2899C900114294 /* BottomSheet in Frameworks */,
|
||||
);
|
||||
@ -81,14 +87,12 @@
|
||||
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
||||
88CDE0502A2508E900114294 /* ContentView.swift */,
|
||||
E253A9202A2B39A700EC6B28 /* Extensions */,
|
||||
88CDE07C2A28AFE700114294 /* Views */,
|
||||
88CDE0792A28AF3E00114294 /* Bluetooth */,
|
||||
88CDE06E2A28AE8D00114294 /* Temperature */,
|
||||
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
||||
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
||||
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
||||
88CDE0502A2508E900114294 /* ContentView.swift */,
|
||||
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
||||
88CDE0542A2508EA00114294 /* Preview Content */,
|
||||
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
||||
@ -122,6 +126,9 @@
|
||||
children = (
|
||||
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
||||
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
||||
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
||||
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
||||
);
|
||||
path = Bluetooth;
|
||||
sourceTree = "<group>";
|
||||
@ -130,10 +137,19 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */,
|
||||
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E253A9202A2B39A700EC6B28 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -153,6 +169,7 @@
|
||||
packageProductDependencies = (
|
||||
88CDE0652A25D08F00114294 /* SFSafeSymbols */,
|
||||
88CDE06A2A2899C900114294 /* BottomSheet */,
|
||||
E253A9262A2CA48A00EC6B28 /* SQLite */,
|
||||
);
|
||||
productName = TempTrack;
|
||||
productReference = 88CDE04B2A2508E900114294 /* TempTrack.app */;
|
||||
@ -185,6 +202,7 @@
|
||||
packageReferences = (
|
||||
88CDE0642A25D08F00114294 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||
88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */,
|
||||
E253A9252A2CA48900EC6B28 /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||
);
|
||||
productRefGroup = 88CDE04C2A2508E900114294 /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -223,6 +241,8 @@
|
||||
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
||||
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
||||
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */,
|
||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
||||
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
||||
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
||||
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
||||
@ -449,6 +469,14 @@
|
||||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@ -462,6 +490,11 @@
|
||||
package = 88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */;
|
||||
productName = BottomSheet;
|
||||
};
|
||||
E253A9262A2CA48A00EC6B28 /* SQLite */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E253A9252A2CA48900EC6B28 /* XCRemoteSwiftPackageReference "SQLite" */;
|
||||
productName = SQLite;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 88CDE0432A2508E800114294 /* Project object */;
|
||||
|
@ -14,8 +14,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||
"state" : {
|
||||
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
||||
"version" : "4.1.1"
|
||||
"revision" : "2bcd249b49178247e6b52bac7d67d6e338a40cee",
|
||||
"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,8 +55,10 @@ final class BluetoothClient: ObservableObject {
|
||||
private var runningTransfer: TemperatureDataTransfer?
|
||||
|
||||
func updateDeviceInfo() {
|
||||
if case .configured = deviceState {
|
||||
addRequest(.getInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private var dataUpdateTimer: Timer?
|
||||
|
||||
|
@ -4,20 +4,56 @@ import BottomSheet
|
||||
|
||||
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
|
||||
var client = BluetoothClient()
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
init(client: BluetoothClient) {
|
||||
self.client = client
|
||||
}
|
||||
@ObservedObject
|
||||
var storage = TemperatureStorage()
|
||||
|
||||
@State
|
||||
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? {
|
||||
let t1 = client.deviceInfo?.sensor1?.optionalValue
|
||||
guard let t0 = client.deviceInfo?.sensor0?.optionalValue else {
|
||||
@ -55,53 +91,76 @@ struct ContentView: View {
|
||||
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 {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemSymbol: .iphone)
|
||||
.frame(width: 30)
|
||||
Text(client.deviceState.text)
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
Image(systemSymbol: temperatureIcon)
|
||||
.font(.system(size: 200, weight: .light))
|
||||
// Image(systemSymbol: temperatureIcon)
|
||||
// .font(.system(size: 100, weight: .light))
|
||||
if hasTemperature {
|
||||
Text(temperatureString)
|
||||
.font(.system(size: 100, weight: .light))
|
||||
.font(.system(size: 150, weight: .light))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
TemperatureHistoryChart(points: storage.lastMeasurements)
|
||||
.frame(height: 150)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
_ = client.collectRecordedData()
|
||||
}) {
|
||||
Text("Transfer")
|
||||
}.padding()
|
||||
Spacer()
|
||||
Button {
|
||||
self.showDeviceInfo = true
|
||||
} label: {
|
||||
Image(systemSymbol: .infoCircle)
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
}.disabled(client.deviceInfo == nil)
|
||||
if hasDeviceInfo {
|
||||
Image(systemSymbol: .iphone)
|
||||
.font(.system(size: 30, weight: .regular))
|
||||
}
|
||||
Text(client.deviceState.text)
|
||||
}
|
||||
.disabled(!hasDeviceInfo)
|
||||
.foregroundColor(.white)
|
||||
|
||||
}.padding()
|
||||
}
|
||||
.padding()
|
||||
.bottomSheet(isPresented: $showDeviceInfo, height: 520) {
|
||||
.bottomSheet(isPresented: $showDeviceInfo, height: 600) {
|
||||
if let info = client.deviceInfo {
|
||||
DeviceInfoView(info: info)
|
||||
DeviceInfoView(
|
||||
info: info,
|
||||
isPresented: $showDeviceInfo, updateToggle: $updateInfoToggle)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
.background(backgroundGradient)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
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
|
||||
|
||||
@main
|
||||
|
@ -1,10 +1,113 @@
|
||||
import Foundation
|
||||
|
||||
struct TemperatureMeasurement {
|
||||
struct TemperatureMeasurement: Identifiable {
|
||||
|
||||
var sensor0: TemperatureValue
|
||||
|
||||
var sensor1: TemperatureValue
|
||||
|
||||
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"
|
||||
case .invalidMeasurement:
|
||||
return "Invalid"
|
||||
case .value(let double):
|
||||
return "\(Int(double.rounded()))°C"
|
||||
case .value(let value):
|
||||
return String(format:" %.1f°C", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,70 @@
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,12 @@ struct DeviceInfoView: View {
|
||||
|
||||
let info: DeviceInfo
|
||||
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
@Binding
|
||||
var updateToggle: Bool
|
||||
|
||||
private var runTimeString: String {
|
||||
let number = info.numberOfSecondsRunning
|
||||
guard number >= 60 else {
|
||||
@ -97,6 +103,16 @@ struct DeviceInfoView: View {
|
||||
|
||||
var body: some View {
|
||||
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) {
|
||||
Text("Recording")
|
||||
.font(.headline)
|
||||
@ -159,8 +175,11 @@ struct DeviceInfoView: View {
|
||||
|
||||
struct DeviceInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DeviceInfoView(info: .mock)
|
||||
.previewLayout(.fixed(width: 375, height: 500))
|
||||
DeviceInfoView(
|
||||
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