Cleanup code

This commit is contained in:
2025-04-23 23:51:18 +02:00
parent d6af1a7438
commit d3143915ea
6 changed files with 72 additions and 106 deletions
+54 -1
View File
@@ -2,7 +2,60 @@
// AsyncImageWithPlaceholder.swift // AsyncImageWithPlaceholder.swift
// meterios // meterios
// //
// Created by Maximilian on 23.04.25. // (c) 2025 Martin "maride" Dessauer
// //
import Foundation import Foundation
import SwiftUI
/// **AsyncImageWithPlaceholder** wraps AsyncImage, showing a Spinner while loading and a question mark in case of errors.
struct AsyncImageWithPlaceholder : View {
private var url: URL?
private var width: CGFloat
private var height: CGFloat
// Access the current color scheme (that is, e.g.: dark mode, light mode) environment value
@Environment(\.colorScheme) var colorScheme
// baseShape and baseShapeView resemble the "background" (while loading, and in case of errors, and for the image itself)
private var baseShape : some Shape = RoundedRectangle(cornerRadius: 8.0)
var baseShapeView : some View {
baseShape
.foregroundStyle(colorScheme == .dark ? .black : .white)
.frame(width: width, height: height)
}
/// Creates a new instance of AsyncImageWithPlaceholder, loading the image pointed to by url, with exact measures of width by height
init(url: URL? = nil, width: CGFloat, height: CGFloat) {
self.url = url
self.width = width
self.height = height
}
var body: some View {
AsyncImage(url: self.url) { phase in
if let image = phase.image {
// Image loaded
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.width, height: self.height)
.clipShape(baseShape)
} else if phase.error != nil {
// Error, display a dummy image (question mark)
ZStack {
baseShapeView
Text("?")
.fontWeight(.bold)
.foregroundStyle(.gray)
}
} else {
// Loading Placeholder
ZStack {
baseShapeView
ProgressView()
}
}
}.fixedSize()
}
}
+9 -36
View File
@@ -9,7 +9,6 @@ import SwiftUI
import WatchConnectivity import WatchConnectivity
enum WCMeteriosMessageTypes: String { enum WCMeteriosMessageTypes: String {
case ConnectionInfo
case DrinkList case DrinkList
case BookDrink case BookDrink
} }
@@ -26,7 +25,6 @@ class WCManager: NSObject, WCSessionDelegate {
if (WCSession.isSupported()) { if (WCSession.isSupported()) {
session.delegate = self session.delegate = self
session.activate() session.activate()
log("Activated session")
} }
} }
@@ -42,13 +40,8 @@ class WCManager: NSObject, WCSessionDelegate {
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
#if os(iOS) #if os(iOS)
// Check for the different possible message types // Check for the different possible message types
if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.ConnectionInfo.rawValue }) { switch message["request"] as! String {
// Request for ConnectionInfo case WCMeteriosMessageTypes.DrinkList.rawValue:
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 // Request for a list of available drinks
Task { Task {
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
@@ -65,7 +58,7 @@ class WCManager: NSObject, WCSessionDelegate {
"availableDrinks": availableDrinkArray "availableDrinks": availableDrinkArray
]) ])
} }
} else if message.contains(where: { $0.key == "request" && $0.value as! String == WCMeteriosMessageTypes.BookDrink.rawValue }) { case WCMeteriosMessageTypes.BookDrink.rawValue:
// Book the given drink at the backend // Book the given drink at the backend
Task { Task {
do { do {
@@ -83,23 +76,15 @@ class WCManager: NSObject, WCSessionDelegate {
]) ])
} }
} }
} else { default:
log("Error: received unknown request. \(message)") // Malformed message from watch
replyHandler([
"error": "request contains no request",
])
} }
#endif // os(iOS) #endif // os(iOS)
} }
// 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 // requestDrinksList requests the list of drinks available at the backend from the paired device
func requestDrinksList(replyHandler: @escaping (Array<Drink>, Array<Drink>) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { func requestDrinksList(replyHandler: @escaping (Array<Drink>, Array<Drink>) -> Void, errorHandler: @escaping ((any Error)?) -> Void) {
session.sendMessage([ session.sendMessage([
@@ -125,21 +110,9 @@ class WCManager: NSObject, WCSessionDelegate {
"request": WCMeteriosMessageTypes.BookDrink.rawValue, "request": WCMeteriosMessageTypes.BookDrink.rawValue,
"drinkID": drinkID, "drinkID": drinkID,
], replyHandler: { reply in ], replyHandler: { reply in
let salut = reply["salut"] as! String replyHandler(reply["salut"] as! String)
replyHandler(salut)
}, errorHandler: { error in }, errorHandler: { error in
errorHandler(error) 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)")
}
} }
+1
View File
@@ -50,6 +50,7 @@
A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = { A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
AsyncImageWithPlaceholder.swift,
Localizable.xcstrings, Localizable.xcstrings,
SalutManager.swift, SalutManager.swift,
types/Drink.swift, types/Drink.swift,
+1 -23
View File
@@ -33,29 +33,7 @@ struct DrinkDetail: View {
Spacer() Spacer()
VStack() { VStack() {
// Displays the drink image asynchronously // Displays the drink image asynchronously
AsyncImage(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)")) { phase in AsyncImageWithPlaceholder(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)"), width: 300, height: 300).fixedSize()
if let image = phase.image {
// Image loaded
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.clipShape(RoundedRectangle(cornerRadius: 8.0))
} else if phase.error != nil {
// Error, display a dummy image (question mark)
let _ = Logger().error("Failed to load drink image for \(drink.name): \(phase.error)")
ZStack {
RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 300, height: 300)
Text("?").fontWeight(.bold)
}
} else {
// Loading Placeholder
ZStack {
RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 300, height: 300)
ProgressView()
}
}
}.fixedSize()
// Basic drink information // Basic drink information
VStack(alignment: .center) { VStack(alignment: .center) {
+6 -36
View File
@@ -15,7 +15,7 @@ struct MainView: View {
@State private var favouriteDrinks: Array<Drink> = [] @State private var favouriteDrinks: Array<Drink> = []
@State private var availableDrinks: Array<Drink> = [] @State private var availableDrinks: Array<Drink> = []
@State private var outOfOrderDrinks: Array<Drink> = [] @State private var outOfOrderDrinks: Array<Drink> = []
@State private var user: User = User.init(meteID: 0, displayName: "", displayImageURL: "", balance: 0.00) @State private var user: User?
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@@ -23,7 +23,9 @@ struct MainView: View {
func update() { func update() {
Task { Task {
do { do {
// Load user
user = try await BackendConnector().GetUser(baseAddr: meteHostAddr, userID: meteUserID) user = try await BackendConnector().GetUser(baseAddr: meteHostAddr, userID: meteUserID)
// Load drinks
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active }) favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active }) availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
@@ -42,29 +44,7 @@ struct MainView: View {
update() update()
}) })
}, label: { }, label: {
AsyncImage(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)")) { phase in AsyncImageWithPlaceholder(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)"), width: 32, height: 32)
if let image = phase.image {
// Image loaded
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32)
.clipShape(RoundedRectangle(cornerRadius: 8.0))
} else if phase.error != nil {
// Error, display a dummy image (question mark)
let _ = Logger().error("Failed to load drink image for \(drink.name): \(phase.error)")
ZStack {
RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 32, height: 32)
Text("?").fontWeight(.bold)
}
} else {
// Loading Placeholder
ZStack {
RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 32, height: 32)
ProgressView()
}
}
}.fixedSize()
Text(drink.name) Text(drink.name)
}) })
} }
@@ -78,23 +58,13 @@ struct MainView: View {
List () { List () {
// Account // Account
Section(header: Text(String(localized: "ACCOUNT")).id("topAnchor")) { Section(header: Text(String(localized: "ACCOUNT")).id("topAnchor")) {
NavigationLink(destination: UserDetailView(user: user)) { NavigationLink(destination: UserDetailView(user: user!)) {
Button(action: { Button(action: {
dismiss() dismiss()
}) { }) {
HStack { HStack {
// Profile Image // Profile Image
AsyncImage( AsyncImageWithPlaceholder(url: URL(string: user!.displayImageURL), width: 64, height: 64).clipShape(Circle()).fixedSize()
url: URL(string: user.displayImageURL),
content: { image in image.resizable().aspectRatio(contentMode: .fill).frame(width: 64, height: 64).clipShape(Circle()) },
placeholder: {
ZStack {
Circle().fill(.gray).frame(width: 64, height: 64)
ProgressView()
}
}
).fixedSize()
Spacer(minLength: 16.0) Spacer(minLength: 16.0)
// Account, Address & Balance // Account, Address & Balance
+1 -10
View File
@@ -22,16 +22,7 @@ struct UserDetailView: View {
Spacer() Spacer()
VStack(alignment: .center) { VStack(alignment: .center) {
// User image // User image
AsyncImage( AsyncImageWithPlaceholder(url: URL(string: user.displayImageURL), width: 256, height: 256).clipShape(Circle())
url: URL(string: user.displayImageURL),
content: { image in image.resizable().aspectRatio(contentMode: .fill).frame(width: 256, height: 256).clipShape(Circle()) },
placeholder: {
ZStack {
Circle().fill(.gray).frame(width: 256, height: 256)
ProgressView()
}
}
).fixedSize()
// Basic info about user // Basic info about user
VStack(alignment: .center) { VStack(alignment: .center) {