TempTrack-iOS/TempTrack/ContentView.swift

291 lines
8.5 KiB
Swift
Raw Permalink Normal View History

2023-05-29 18:23:13 +02:00
import SwiftUI
2023-06-03 08:15:00 +02:00
import SFSafeSymbols
2023-05-29 18:23:13 +02:00
struct ContentView: View {
private let deviceInfoUpdateInterval = 3.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)
2023-07-03 13:28:51 +02:00
@ObservedObject
var scanner: BluetoothScanner
@EnvironmentObject
var storage: PersistentStorage
2023-07-03 13:28:51 +02:00
@EnvironmentObject
var transfer: TransferHandler
@State
var showDeviceInfo = false
@State
var showHistory = false
2023-06-14 16:16:56 +02:00
@State
var showLog = false
@State
var showDataTransferView = false
@State
var deviceInfoUpdateTimer: Timer?
2023-06-03 08:15:00 +02:00
var averageTemperature: Double? {
2023-07-03 13:28:51 +02:00
guard scanner.configuredDevice != nil else {
return nil
}
2023-07-03 13:28:51 +02:00
guard let info = scanner.lastDeviceInfo else {
return nil
}
let t1 = info.sensor1?.optionalValue
guard let t0 = info.sensor0?.optionalValue else {
2023-06-03 08:15:00 +02:00
return t1
}
guard let t1 else {
return t0
}
return (t0 + t1) / 2
}
var hasTemperature: Bool {
averageTemperature != nil
}
var temperatureString: String {
guard let temp = averageTemperature else {
return "?"
}
return String(format: "%.0f°", locale: .current, temp)
}
var temperatureIcon: SFSymbol {
guard let temp = averageTemperature else {
return .thermometerMediumSlash
}
guard temp > 0 else {
return .thermometerSnowflake
}
guard temp > 15 else {
return .thermometerLow
}
guard temp > 25 else {
return .thermometerMedium
}
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 connectionSymbol: SFSymbol {
if scanner.configuredDevice != nil {
return .iphoneCircle
}
if !scanner.bluetoothIsAvailable {
return .antennaRadiowavesLeftAndRightSlash
}
switch scanner.connectionState {
case .noDeviceFound:
if scanner.isScanningForDevices {
return .iphoneRadiowavesLeftAndRightCircle
}
return .iphoneSlashCircle
case .connecting:
return .arrowRightToLineCircle
case .discoveringService:
return .linkCircle
case .discoveringCharacteristic:
return .magnifyingglassCircle
}
}
var hasNoDeviceInfo: Bool {
2023-07-03 13:28:51 +02:00
scanner.lastDeviceInfo == nil
}
var isDisconnected: Bool {
scanner.configuredDevice == nil
}
2023-05-29 18:23:13 +02:00
var body: some View {
VStack {
2023-06-03 08:15:00 +02:00
Spacer()
if hasTemperature {
Text(temperatureString)
.font(.system(size: 150, weight: .light))
.foregroundColor(.white)
} else {
Image(systemSymbol: temperatureIcon)
.font(.system(size: 100, weight: .thin))
.foregroundColor(.gray)
2023-06-03 08:15:00 +02:00
}
2023-06-03 08:15:00 +02:00
Spacer()
Button {
self.showHistory = true
} label: {
ZStack {
TemperatureHistoryChart(points: $storage.recentMeasurements)
.frame(height: 300)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
if storage.recentMeasurements.isEmpty {
Text("No recent measurements")
2023-07-03 13:28:51 +02:00
.foregroundColor(.white)
}
}
}
2023-06-03 08:15:00 +02:00
HStack(alignment: .center) {
2023-06-14 16:16:56 +02:00
Button {
self.showLog = true
2023-06-14 16:16:56 +02:00
} label: {
Image(systemSymbol: .paperclipCircle)
2023-06-14 16:16:56 +02:00
.foregroundColor(.white)
}
Spacer()
2023-06-03 08:15:00 +02:00
Button {
2023-07-03 13:28:51 +02:00
if scanner.isScanningForDevices {
scanner.isScanningForDevices = false
} else if scanner.isConnectingOrConnected {
scanner.disconnect()
} else {
scanner.isScanningForDevices = true
}
2023-06-03 08:15:00 +02:00
} label: {
Image(systemSymbol: connectionSymbol)
.foregroundColor(.white)
}
.foregroundColor(.white)
2023-06-14 16:16:56 +02:00
Spacer()
2023-07-03 13:28:51 +02:00
if scanner.lastDeviceInfo != nil {
Button {
self.showDeviceInfo = true
} label: {
Image(systemSymbol: .infoCircle)
2023-07-03 13:28:51 +02:00
.foregroundColor(.white)
}
Spacer()
Button {
showDataTransferView = true
} label: {
Image(systemSymbol: .arrowUpArrowDownCircle)
.foregroundColor(.white)
}
} else {
Image(systemSymbol: .infoCircle)
.foregroundColor(.gray)
Spacer()
2023-06-14 16:16:56 +02:00
Image(systemSymbol: .arrowUpArrowDownCircle)
.foregroundColor(.gray)
}
}
.padding()
.font(.system(size: 30, weight: .light))
2023-05-29 18:23:13 +02:00
}
.padding()
2023-06-08 14:57:40 +02:00
.sheet(isPresented: $showDeviceInfo) {
2023-07-03 13:28:51 +02:00
if let info = scanner.lastDeviceInfo {
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
2023-06-03 08:15:00 +02:00
} else {
EmptyView()
}
}
.sheet(isPresented: $showHistory) {
HistoryList()
.environmentObject(storage)
}
2023-06-14 16:16:56 +02:00
.sheet(isPresented: $showLog) {
LogView()
.environmentObject(log)
2023-07-03 13:28:51 +02:00
.environmentObject(storage)
2023-06-14 16:16:56 +02:00
}
.sheet(isPresented: $showDataTransferView) {
if let client = scanner.configuredDevice {
TransferView(
2023-07-03 13:28:51 +02:00
bluetoothClient: client, info: $scanner.lastDeviceInfo)
.environmentObject(storage)
2023-07-03 13:28:51 +02:00
.environmentObject(transfer)
} else {
EmptyView()
}
}
.background(backgroundGradient)
.onAppear(perform: startDeviceInfoUpdates)
.onDisappear(perform: endDeviceInfoUpdates)
}
// MARK: Device info updates
private func startDeviceInfoUpdates() {
deviceInfoUpdateTimer?.invalidate()
log.info("Starting device info updates")
deviceInfoUpdateTimer = Timer.scheduledTimer(withTimeInterval: deviceInfoUpdateInterval, repeats: true) { timer in
self.updateDeviceInfo()
}
deviceInfoUpdateTimer?.fire()
}
private func updateDeviceInfo() {
guard let bluetoothDevice = scanner.configuredDevice else {
return
}
Task {
await bluetoothDevice.updateInfo()
}
}
private func endDeviceInfoUpdates() {
deviceInfoUpdateTimer?.invalidate()
deviceInfoUpdateTimer = nil
2023-05-29 18:23:13 +02:00
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
2023-07-03 13:28:51 +02:00
ContentView(scanner: .init())
2023-06-11 21:57:07 +02:00
.environmentObject(storage)
2023-05-29 18:23:13 +02:00
}
}
2023-06-03 08:15:00 +02:00
extension HorizontalAlignment {
private struct InfoTextAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading]
}
}
static let infoTextAlignmentGuide = HorizontalAlignment(
InfoTextAlignment.self
)
}