import SwiftUI 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() @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 { 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 body: some View { VStack { Spacer() // Image(systemSymbol: temperatureIcon) // .font(.system(size: 100, weight: .light)) if hasTemperature { Text(temperatureString) .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 { self.showDeviceInfo = true } label: { 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: 600) { if let info = client.deviceInfo { 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), values: TemperatureMeasurement.mockData) } } extension HorizontalAlignment { private struct InfoTextAlignment: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context[HorizontalAlignment.leading] } } static let infoTextAlignmentGuide = HorizontalAlignment( InfoTextAlignment.self ) }