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
+1 -1
View File
@@ -2,6 +2,6 @@
# meterios
*Pick your poison*, now on iOS.
*Pick your poison*, now on iOS & watchOS.
+18
View File
@@ -0,0 +1,18 @@
//
// SalutManager.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
class SalutManager {
static let salutes: Array<String> = [
"Prost!", "Guten!", "Kippis!", "Cheers!", "Chin-Chin!", "Zum Wohl!", "Salut!", "לחיים",
"Jamas!", // thanks cocorilla
"Budmo!", "за здоровье!", "Sláinte!", "乾杯", "Noroc!", // thanks ChaosAyumi
]
static func getSalute() -> String {
return salutes[Int.random(in: 0..<salutes.count)]
}
}
+147
View File
@@ -0,0 +1,147 @@
//
// WCManager.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
import WatchConnectivity
enum WCMeteriosMessageTypes: String {
case ConnectionInfo
case DrinkList
case BookDrink
}
class WCManager: NSObject, WCSessionDelegate {
@AppStorage("meteHostAddr") private var meteHostAddr = ""
@AppStorage("meteUserID") private var meteUserID = -1
private let session: WCSession = WCSession.default
override init() {
super.init()
if (WCSession.isSupported()) {
session.delegate = self
session.activate()
log("Activated session")
}
}
// Functions required by WCSessionDelegate protocol, but not of interest for Meterios
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {}
#endif // os(iOS)
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {}
// session(...) gets called for every incoming message through the WatchConnectivity channel
// As Meterios currently only needs messages from watch to device and responses back to the watch, this function currently only implements device-related responses.
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
// Check for the different possible message types
if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.ConnectionInfo.rawValue }) {
// Request for ConnectionInfo
replyHandler([
"userID": meteUserID,
"hostAddr": meteHostAddr
])
} else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.DrinkList.rawValue }) {
// Request for a list of available drinks
Task {
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
let availableDrinks = drinks.filter({ $0.active })
// Create a plist-compatible variant of the Drink object containing the required information
var drinkArray: Array<String> = []
for d in availableDrinks {
drinkArray.append(
try! JSONEncoder().encode(d).base64EncodedString()
)
}
replyHandler([
"drinks": drinkArray
])
}
} else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.BookDrink.rawValue }) {
// Book the given drink at the backend
Task {
do {
// Book
let drinkID = message["drinkID"] as! Int
try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID)
replyHandler([
"salut": SalutManager.getSalute()
])
} catch {
// Error booking against the backend
replyHandler([
"error": error,
"salut": ""
])
}
}
} else {
log("Error: received unknown request. \(message)")
}
}
// requestConnectionInfo requests User ID and Host address from the paired device
func requestConnectionInfo(replyHandler: @escaping (Int, String) -> Void, errorHandler: @escaping ((any Error)?) -> Void) {
session.sendMessage([
"request": WCMeteriosMessageTypes.ConnectionInfo.rawValue
], replyHandler: { reply in
replyHandler(reply["userID"] as! Int, reply["hostAddr"] as! String)
}, errorHandler: { error in
errorHandler(error)
})
}
// requestDrinksList requests the list of drinks available at the backend from the paired device
func requestDrinksList(replyHandler: @escaping (Array<Drink>) -> Void, errorHandler: @escaping ((any Error)?) -> Void) {
session.sendMessage([
"request": WCMeteriosMessageTypes.DrinkList.rawValue
], replyHandler: { reply in
let drinks = reply["drinks"] as! Array<String>
// Create Drink objects from response
var drinkArray: Array<Drink> = []
for d in drinks {
let jsonDecoded = Data(base64Encoded: d)
let drink = try! JSONDecoder().decode(Drink.self, from: jsonDecoded!)
drinkArray.append(drink)
}
// enjoy your drinks, replyHandler!
replyHandler(drinkArray)
}, errorHandler: { error in
errorHandler(error)
})
}
// bookDrink books the drink given by drinkID
func bookDrink(drinkID: Int, replyHandler: @escaping (String) -> Void, errorHandler: @escaping ((any Error)?) -> Void) {
session.sendMessage([
"request": WCMeteriosMessageTypes.BookDrink.rawValue,
"drinkID": drinkID,
], replyHandler: { reply in
let salut = reply["salut"] as! String
replyHandler(salut)
}, errorHandler: { error in
errorHandler(error)
})
}
private func log(_ line: String) {
#if os(iOS)
let prefix = "[WCM@iOS]"
#elseif os(watchOS)
let prefix = "[WCM@wOS]"
#else
let prefix = "[WCM@?OS]"
#endif // os(...)
print("\(prefix) \(line)")
}
}
+52
View File
@@ -0,0 +1,52 @@
//
// Drink.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
struct Drink: Identifiable, Encodable, Decodable {
public var id: Int
public var name: String
public var logoURL: String
private var price: String
private var bottle_size: String
public var caffeine: Int
public var active: Bool
// KeyMap is an enum covering the field names as used by SpaceMarket API
private enum KeyMap : String, CodingKey { case id, name, bottle_size, caffeine, price, active, logo_url }
// Init function to be used with JSONDecoder using SpaceMarket API's strange types
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: KeyMap.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.bottle_size = (try? container.decode(String.self, forKey: .bottle_size)) ?? ""
self.caffeine = (try? container.decode(Int.self, forKey: .caffeine)) ?? 0
self.price = (try? container.decode(String.self, forKey: .price)) ?? ""
self.active = (try? container.decode(Bool.self, forKey: .active)) ?? false
self.logoURL = (try? container.decode(String.self, forKey: .logo_url)) ?? ""
}
// Init function, as straight-forward as it gets
init(id: Int, name: String, logoURL: String = "", price: String = "", bottle_size: String = "", caffeine: Int = 0, active: Bool = true) {
self.id = id
self.name = name
self.logoURL = logoURL
self.price = price
self.bottle_size = bottle_size
self.caffeine = caffeine
self.active = active
}
func GetPrice() -> Float {
return Float(self.price) ?? 9999.99
}
func GetBottleSize() -> Float {
return Float(self.bottle_size) ?? 9999.99
}
}
@@ -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()
}
})
}
}
+195 -2
View File
@@ -6,16 +6,81 @@
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
A7FA4CB72D9E8FF5005ACDBB /* meterios-watch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
A7FA4CB52D9E8FF5005ACDBB /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A7F97A862D8DD542004EF4ED /* Project object */;
proxyType = 1;
remoteGlobalIDString = A7FA4CA92D9E8FF1005ACDBB;
remoteInfo = "meterios-watch Watch App";
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
A7C3C98C2D9D67EB00731E27 /* Embed Watch Content */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
dstSubfolderSpec = 16;
files = (
A7FA4CB72D9E8FF5005ACDBB /* meterios-watch.app in Embed Watch Content */,
);
name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
A7F97A8E2D8DD542004EF4ED /* meterios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meterios.app; sourceTree = BUILT_PRODUCTS_DIR; };
A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "meterios-watch.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
A7FA4CBE2D9E90B0005ACDBB /* Exceptions for "meterios" folder in "meterios-watch" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
BackendConnector.swift,
);
target = A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */;
};
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
SalutManager.swift,
types/Drink.swift,
types/User.swift,
WCManager.swift,
);
target = A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
A7F97A902D8DD542004EF4ED /* meterios */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
A7FA4CBE2D9E90B0005ACDBB /* Exceptions for "meterios" folder in "meterios-watch" target */,
);
path = meterios;
sourceTree = "<group>";
};
A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "meterios-watch";
sourceTree = "<group>";
};
A7FA4CC62D9E94BD005ACDBB /* common */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */,
);
path = common;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -26,13 +91,22 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
A7FA4CA72D9E8FF1005ACDBB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A7F97A852D8DD542004EF4ED = {
isa = PBXGroup;
children = (
A7FA4CC62D9E94BD005ACDBB /* common */,
A7F97A902D8DD542004EF4ED /* meterios */,
A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */,
A7F97A8F2D8DD542004EF4ED /* Products */,
);
sourceTree = "<group>";
@@ -41,6 +115,7 @@
isa = PBXGroup;
children = (
A7F97A8E2D8DD542004EF4ED /* meterios.app */,
A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */,
);
name = Products;
sourceTree = "<group>";
@@ -55,13 +130,16 @@
A7F97A8A2D8DD542004EF4ED /* Sources */,
A7F97A8B2D8DD542004EF4ED /* Frameworks */,
A7F97A8C2D8DD542004EF4ED /* Resources */,
A7C3C98C2D9D67EB00731E27 /* Embed Watch Content */,
);
buildRules = (
);
dependencies = (
A7FA4CB62D9E8FF5005ACDBB /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
A7F97A902D8DD542004EF4ED /* meterios */,
A7FA4CC62D9E94BD005ACDBB /* common */,
);
name = meterios;
packageProductDependencies = (
@@ -70,6 +148,28 @@
productReference = A7F97A8E2D8DD542004EF4ED /* meterios.app */;
productType = "com.apple.product-type.application";
};
A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */ = {
isa = PBXNativeTarget;
buildConfigurationList = A7FA4CB82D9E8FF5005ACDBB /* Build configuration list for PBXNativeTarget "meterios-watch" */;
buildPhases = (
A7FA4CA62D9E8FF1005ACDBB /* Sources */,
A7FA4CA72D9E8FF1005ACDBB /* Frameworks */,
A7FA4CA82D9E8FF1005ACDBB /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
A7FA4CAB2D9E8FF1005ACDBB /* meterios-watch */,
);
name = "meterios-watch";
packageProductDependencies = (
);
productName = "meterios-watch Watch App";
productReference = A7FA4CAA2D9E8FF1005ACDBB /* meterios-watch.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -83,6 +183,9 @@
A7F97A8D2D8DD542004EF4ED = {
CreatedOnToolsVersion = 16.2;
};
A7FA4CA92D9E8FF1005ACDBB = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = A7F97A892D8DD542004EF4ED /* Build configuration list for PBXProject "meterios" */;
@@ -100,6 +203,7 @@
projectRoot = "";
targets = (
A7F97A8D2D8DD542004EF4ED /* meterios */,
A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */,
);
};
/* End PBXProject section */
@@ -112,6 +216,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
A7FA4CA82D9E8FF1005ACDBB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -122,8 +233,23 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
A7FA4CA62D9E8FF1005ACDBB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
A7FA4CB62D9E8FF5005ACDBB /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = A7FA4CA92D9E8FF1005ACDBB /* meterios-watch */;
targetProxy = A7FA4CB52D9E8FF5005ACDBB /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
A7F97A9A2D8DD54B004EF4ED /* Debug */ = {
isa = XCBuildConfiguration;
@@ -272,7 +398,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -309,7 +435,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -322,6 +448,64 @@
};
name = Release;
};
A7FA4CB92D9E8FF5005ACDBB /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3RNVF52786;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Meterios;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = space.chaosdorf.meterios;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 8.7;
};
name = Debug;
};
A7FA4CBA2D9E8FF5005ACDBB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3RNVF52786;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Meterios;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = space.chaosdorf.meterios;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 8.7;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -343,6 +527,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A7FA4CB82D9E8FF5005ACDBB /* Build configuration list for PBXNativeTarget "meterios-watch" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A7FA4CB92D9E8FF5005ACDBB /* Debug */,
A7FA4CBA2D9E8FF5005ACDBB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = A7F97A862D8DD542004EF4ED /* Project object */;
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7FA4CA92D9E8FF1005ACDBB"
BuildableName = "meterios-watch.app"
BlueprintName = "meterios-watch"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
BuildableName = "meterios.app"
BlueprintName = "meterios"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7FA4CA92D9E8FF1005ACDBB"
BuildableName = "meterios-watch.app"
BlueprintName = "meterios-watch"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7FA4CA92D9E8FF1005ACDBB"
BuildableName = "meterios-watch.app"
BlueprintName = "meterios-watch"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
BuildableName = "meterios.app"
BlueprintName = "meterios"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
BuildableName = "meterios.app"
BlueprintName = "meterios"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7F97A8D2D8DD542004EF4ED"
BuildableName = "meterios.app"
BlueprintName = "meterios"
ReferencedContainer = "container:meterios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+2
View File
@@ -11,6 +11,8 @@ import SwiftUI
struct meteriosApp: App {
@AppStorage("meteHostAddr") private var meteHostAddr = ""
@AppStorage("meteUserID") private var meteUserID = -1
private var wcMgr = WCManager()
var body: some Scene {
WindowGroup {
-33
View File
@@ -1,33 +0,0 @@
//
// Drink.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
import SwiftUI
struct Drink: Identifiable, Decodable {
var id: Int
var name: String
var logoURL: String = ""
var price: Float = 0.00
var bottle_size: Float = 0.00
var caffeine: Int = 0
var active: Bool = true
// KeyMap is an enum covering the field names as used by SpaceMarket API
private enum KeyMap : String, CodingKey { case id, name, bottle_size, caffeine, price, active, logo_url }
// Init function to be used with JSONDecoder, converting from SpaceMarket API's strange types to proper types
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: KeyMap.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.bottle_size = Float(try container.decode(String.self, forKey: .bottle_size)) ?? 0.00
self.caffeine = (try? container.decode(Int.self, forKey: .caffeine)) ?? 0
self.price = Float(try container.decode(String.self, forKey: .price)) ?? 9999.99
self.active = (try? container.decode(Bool.self, forKey: .active)) ?? false
self.logoURL = (try? container.decode(String.self, forKey: .logo_url)) ?? ""
}
}
+2 -2
View File
@@ -65,7 +65,7 @@ struct DrinkDetail: View {
}.fixedSize()
Divider()
VStack {
Text(String(format: "%.2f€", drink.price)).fontWeight(.bold)
Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold)
Text("Price").fontWeight(Font.Weight.thin)
}
}
@@ -89,7 +89,7 @@ struct DrinkDetail: View {
HStack {
Text("Total sum:")
Spacer()
Text(String(format: "%.2f€", self.drink.price * Float(self.drinkAmount))).fontWeight(.bold)
Text(String(format: "%.2f€", self.drink.GetPrice() * Float(self.drinkAmount))).fontWeight(.bold)
}
}
+1 -10
View File
@@ -25,15 +25,6 @@ struct TransactionView: View {
@State var backendBookTimer: Timer?
@State var backendTimerReady: Bool = false
let salutes: Array<String> = [
"Prost!", "Guten!", "Kippis!", "Cheers!", "Chin-Chin!", "Zum Wohl!", "Salut!", "לחיים",
"Jamas!", // thanks cocorilla
"Budmo!", "за здоровье!", "Sláinte!", "乾杯", "Noroc!", // thanks ChaosAyumi
]
func getSalute() -> String {
return salutes[Int.random(in: 0..<salutes.count)]
}
init(drinkID: Int, drinkAmount: Int) {
self.drinkID = drinkID
self.drinkAmount = drinkAmount
@@ -54,7 +45,7 @@ struct TransactionView: View {
for _ in 1...drinkAmount {
try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID)
}
statusLine = getSalute()
statusLine = SalutManager.getSalute()
backendTimerReady = true
maybeDismiss()
} catch {