Cleanup code
This commit is contained in:
@@ -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
@@ -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)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user