TempTrack-iOS/TempTrack/ContentView.swift
2023-07-02 17:29:39 +02:00

283 lines
8.1 KiB
Swift

import SwiftUI
import SFSafeSymbols
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)
@StateObject
var scanner = BluetoothScanner()
@EnvironmentObject
var storage: PersistentStorage
@State
var showDeviceInfo = false
@State
var showHistory = false
@State
var showLog = false
@State
var showDataTransferView = false
@State
var deviceInfoUpdateTimer: Timer?
init() { }
var averageTemperature: Double? {
guard let bluetoothDevice = scanner.configuredDevice else {
return nil
}
guard let info = bluetoothDevice.lastDeviceInfo else {
return nil
}
let t1 = info.sensor1?.optionalValue
guard let t0 = info.sensor0?.optionalValue else {
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 {
scanner.configuredDevice?.lastDeviceInfo == nil
}
var isDisconnected: Bool {
scanner.configuredDevice == nil
}
var body: some View {
VStack {
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)
}
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")
}
}
}
HStack(alignment: .center) {
Button {
self.showLog = true
} label: {
Image(systemSymbol: .paperclipCircle)
.foregroundColor(.white)
}
Spacer()
Button {
self.scanner.isScanningForDevices.toggle()
} label: {
Image(systemSymbol: connectionSymbol)
.foregroundColor(.white)
}
.foregroundColor(.white)
Spacer()
if let device = scanner.configuredDevice {
Button {
self.showDeviceInfo = true
} label: {
Image(systemSymbol: .infoCircle)
.foregroundColor(device.lastDeviceInfo == nil ? .gray : .white)
}.disabled(device.lastDeviceInfo == nil)
Spacer()
Button {
showDataTransferView = true
} label: {
Image(systemSymbol: .arrowUpArrowDownCircle)
.foregroundColor(.white)
}
} else {
Image(systemSymbol: .infoCircle)
.foregroundColor(.gray)
Spacer()
Image(systemSymbol: .arrowUpArrowDownCircle)
.foregroundColor(.gray)
}
}
.padding()
.font(.system(size: 30, weight: .light))
}
.padding()
.sheet(isPresented: $showDeviceInfo) {
if let info = scanner.configuredDevice?.lastDeviceInfo {
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
} else {
EmptyView()
}
}
.sheet(isPresented: $showHistory) {
HistoryList()
.environmentObject(storage)
}
.sheet(isPresented: $showLog) {
LogView()
.environmentObject(log)
}
.sheet(isPresented: $showDataTransferView) {
if let client = scanner.configuredDevice {
TransferView(
bluetoothClient: client)
.environmentObject(storage)
} 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
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
ContentView()
.environmentObject(storage)
}
}
extension HorizontalAlignment {
private struct InfoTextAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading]
}
}
static let infoTextAlignmentGuide = HorizontalAlignment(
InfoTextAlignment.self
)
}