Add watchOS support

This commit is contained in:
2025-04-04 18:23:12 +02:00
parent 96b0466deb
commit 384ee97cb7
20 changed files with 857 additions and 48 deletions
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "mete-new.jpg",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+19
View File
@@ -0,0 +1,19 @@
//
// meteriosWatchApp.swift
// meterios-watch
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
@main
struct meteriosWatchApp: App {
private var wcMgr = WCManager()
var body: some Scene {
WindowGroup {
ContentView(wcMgr)
}
}
}
+81
View File
@@ -0,0 +1,81 @@
//
// ContentView.swift
// meterios-watch
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
struct ContentView: View {
private var wcMgr: WCManager
@State private var drinks: Array<Drink> = []
@State private var showUpdateError: Bool = false
@State private var updateError: String = ""
@State private var updateErrorCount: Int = 0
init(_ wcMgr: WCManager) {
self.wcMgr = wcMgr
}
// update performs a full update from the paired device, covering connection info (user ID and backend address) and list of drinks
func update() {
if updateErrorCount >= 3 {
showUpdateError = true
updateErrorCount = 0
return
}
// Update drinks list from paired device
wcMgr.requestDrinksList(
replyHandler: { drinks in
// Drinks received from backend
self.drinks = drinks
showUpdateError = false
}, errorHandler: { error in
// Error receiving drinks
print("Error receiving drinks: \(error)")
updateError = error?.localizedDescription ?? "Unknown error"
updateErrorCount += 1
update()
}
)
}
var body: some View {
ZStack {
if drinks.count > 0 {
NavigationView {
List() {
// Drinks
Section("Drinks") {
ForEach(drinks) { drink in
NavigationLink(drink.name) {
DrinkDetailView(wcMgr: wcMgr, drink: drink)
}
}
}
// Controls
Section {
Button("Refresh") {
drinks = []
update()
}
}
}
}
} else {
ProgressView()
}
}
.onAppear(perform: update)
// Alert to communicate an error in communication with the paired device
.alert("Connection Error", isPresented: $showUpdateError) {
Text(updateError).tint(.red).font(.footnote)
Button("Retry", systemImage: "arrow.clockwise", action: update)
}
}
}
@@ -0,0 +1,58 @@
//
// DrinkDetailView.swift
// meterios-watch
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
struct DrinkDetailView: View {
private var wcMgr: WCManager
private var drink: Drink
@State var shouldShowTransactionSheet = false
@Environment(\.dismiss) private var dismiss
init(wcMgr: WCManager, drink: Drink) {
self.wcMgr = wcMgr
self.drink = drink
}
var body: some View {
VStack {
// Title
Text(drink.name).font(.title)
// Basic info
HStack {
VStack {
Text(String(format: "%img", drink.caffeine)).fontWeight(.bold)
Text("Caffeine").fontWeight(Font.Weight.thin)
}
Divider()
VStack {
Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold)
Text("Price").fontWeight(Font.Weight.thin)
}
}
// Pick button
Button(action: {
// Open the transaction sheet
shouldShowTransactionSheet = true
}) {
Text("Poison picked!")
}.tint(.green)
}
// Transaction sheet
.sheet(isPresented: $shouldShowTransactionSheet) {
// Sheet dismissed - dismiss detail perspective as well.
dismiss()
} content: {
TransactionView(wcMgr: wcMgr, drinkID: drink.id).interactiveDismissDisabled()
}
}
}
@@ -0,0 +1,80 @@
//
// TransactionView.swift
// meterios-watch
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
struct TransactionView: View {
private var wcMgr: WCManager
private var drinkID: Int
@State var statusLine = "Booking..."
@Environment(\.dismiss) private var dismiss
// If the backend reacts too fast (we hate that, huh?), the user is confused because of the short life of this view
// closeTimer takes care of that confusion and gives the user the good old 56k modem feeling.
@State var closeTimer: Timer?
@State var closeTimerReady: Bool = false
@State var backendBookTimer: Timer?
@State var backendTimerReady: Bool = false
init(wcMgr: WCManager, drinkID: Int) {
self.wcMgr = wcMgr
self.drinkID = drinkID
}
// maybeDismiss dismisses this view after the backend call(s) finished AND the timer(s) ran out
func maybeDismiss() {
if closeTimerReady && backendTimerReady {
dismiss()
}
}
// Books the drinks handed over to this view
func book() {
// Try to book the drink(s)
wcMgr.bookDrink(drinkID: drinkID) { salut in
statusLine = salut
backendTimerReady = true
maybeDismiss()
} errorHandler: { error in
statusLine = error!.localizedDescription
print("Error booking drinks: \(error)")
}
}
var body: some View {
// Green checkmark or progress indicator
VStack {
if backendTimerReady {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} else {
ProgressView()
}
// Status line, on success with salute
Text(statusLine)
.font(.title)
// Let watch vibrate on success
if #available(watchOS 10.0, *) {
Text("")
.opacity(0)
.sensoryFeedback(.success, trigger: backendTimerReady)
}
}.onAppear(perform: {
closeTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { timer in
closeTimerReady = true
maybeDismiss()
}
backendBookTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { timer in
book()
}
})
}
}