Atoll/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift
2025-10-27 00:40:03 +05:30

181 lines
6.2 KiB
Swift

import SwiftUI
struct LockScreenWeatherWidget: View {
let snapshot: LockScreenWeatherSnapshot
private let primaryFont = Font.system(size: 22, weight: .semibold, design: .rounded)
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 16) {
if let charging = snapshot.charging {
chargingSegment(for: charging)
}
if let bluetooth = snapshot.bluetooth {
bluetoothSegment(for: bluetooth)
}
weatherSegment
if shouldShowLocation {
locationSegment
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.white.opacity(0.65))
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.clear)
.shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3)
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
}
private var weatherSegment: some View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Image(systemName: snapshot.symbolName)
.font(.system(size: 26, weight: .medium))
.symbolRenderingMode(.hierarchical)
Text(snapshot.temperatureText)
.font(primaryFont)
.kerning(-0.3)
.lineLimit(1)
.minimumScaleFactor(0.9)
.layoutPriority(2)
}
}
@ViewBuilder
private func chargingSegment(for info: LockScreenWeatherSnapshot.ChargingInfo) -> some View {
if let iconName = chargingIconName(for: info) {
HStack(spacing: 4) {
Image(systemName: iconName)
.font(.system(size: 20, weight: .semibold))
.symbolRenderingMode(.hierarchical)
if let text = formattedChargingTime(for: info) {
Text(text)
.font(primaryFont)
.lineLimit(1)
.minimumScaleFactor(0.75)
.layoutPriority(1.2)
}
}
.layoutPriority(1)
}
}
@ViewBuilder
private func bluetoothSegment(for info: LockScreenWeatherSnapshot.BluetoothInfo) -> some View {
HStack(spacing: 4) {
Image(systemName: info.iconName)
.font(.system(size: 20, weight: .semibold))
.symbolRenderingMode(.hierarchical)
Text("\(clampedBatteryLevel(info.batteryLevel))%")
.font(primaryFont)
.lineLimit(1)
.truncationMode(.tail)
.minimumScaleFactor(0.85)
.layoutPriority(1.4)
}
}
@ViewBuilder
private var locationSegment: some View {
if let locationName = snapshot.locationName {
Text(locationName)
.font(primaryFont)
.lineLimit(1)
.truncationMode(.tail)
.minimumScaleFactor(0.75)
.layoutPriority(0.7)
}
}
private var shouldShowLocation: Bool {
snapshot.showsLocation && (snapshot.locationName?.isEmpty == false)
}
private func chargingIconName(for info: LockScreenWeatherSnapshot.ChargingInfo) -> String? {
let icon = info.iconName
return icon.isEmpty ? nil : icon
}
private func formattedChargingTime(for info: LockScreenWeatherSnapshot.ChargingInfo) -> String? {
guard let minutes = info.minutesRemaining, minutes > 0 else {
return nil
}
let hours = minutes / 60
let remainingMinutes = minutes % 60
if hours > 0 {
return "\(hours)h \(remainingMinutes)m"
}
return "\(remainingMinutes)m"
}
private func clampedBatteryLevel(_ level: Int) -> Int {
min(max(level, 0), 100)
}
private var accessibilityLabel: String {
var components: [String] = []
if snapshot.showsLocation, let locationName = snapshot.locationName, !locationName.isEmpty {
components.append(
String(
format: NSLocalizedString("Weather: %@ %@ in %@", comment: "Weather description, temperature, and location"),
snapshot.description,
snapshot.temperatureText,
locationName
)
)
} else {
components.append(
String(
format: NSLocalizedString("Weather: %@ %@", comment: "Weather description and temperature"),
snapshot.description,
snapshot.temperatureText
)
)
}
if let charging = snapshot.charging {
components.append(accessibilityChargingText(for: charging))
}
if let bluetooth = snapshot.bluetooth {
components.append(accessibilityBluetoothText(for: bluetooth))
}
return components.joined(separator: ". ")
}
private func accessibilityChargingText(for charging: LockScreenWeatherSnapshot.ChargingInfo) -> String {
if let minutes = charging.minutesRemaining, minutes > 0 {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .full
let duration = formatter.string(from: TimeInterval(minutes * 60)) ?? "\(minutes) minutes"
return String(
format: NSLocalizedString("Battery charging, %@ remaining", comment: "Charging time remaining"),
duration
)
}
if charging.isPluggedIn && !charging.isCharging {
return NSLocalizedString("Battery fully charged", comment: "Battery is full")
}
return NSLocalizedString("Battery charging", comment: "Battery charging without estimate")
}
private func accessibilityBluetoothText(for bluetooth: LockScreenWeatherSnapshot.BluetoothInfo) -> String {
return String(
format: NSLocalizedString("Bluetooth device %@ at %d percent", comment: "Bluetooth device battery"),
bluetooth.deviceName,
bluetooth.batteryLevel
)
}
}