Prettify main view, add temperature history

This commit is contained in:
Christoph Hagen
2023-06-05 13:05:57 +02:00
parent 6e0910e47f
commit 002eb11dc1
16 changed files with 454 additions and 55 deletions

View File

@ -55,7 +55,9 @@ final class BluetoothClient: ObservableObject {
private var runningTransfer: TemperatureDataTransfer?
func updateDeviceInfo() {
addRequest(.getInfo)
if case .configured = deviceState {
addRequest(.getInfo)
}
}
private var dataUpdateTimer: Timer?

View File

@ -3,20 +3,56 @@ import SFSafeSymbols
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
@ -54,54 +90,77 @@ 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)
}
}

View 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)
}
}

View File

@ -1,10 +1,3 @@
//
// TempTrackApp.swift
// TempTrack
//
// Created by iMac on 29.05.23.
//
import SwiftUI
@main

View File

@ -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)
}
}()
}

View File

@ -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)
}
}
}

View File

@ -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"
}
}

View File

@ -14,6 +14,12 @@ struct DeviceInfoView: View {
private let storageWarnBytes = 500
let info: DeviceInfo
@Binding
var isPresented: Bool
@Binding
var updateToggle: Bool
private var runTimeString: String {
let number = info.numberOfSecondsRunning
@ -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))
}
}

View 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)
}
}