Replace lazy AppStorage calls with AccountManager

This commit is contained in:
2025-04-25 14:26:05 +02:00
parent d3143915ea
commit 351f08e43e
16 changed files with 392 additions and 270 deletions
+84
View File
@@ -0,0 +1,84 @@
//
// AccountManager.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
import Foundation
import SwiftUI
/// **AccountManager** stores connection details and the corresponding client
class AccountManager: ObservableObject {
// default contains a ready-to-use instance of AccountManager, read from AppStorage
static public var `default` = AccountManager()
// accounts holds all saved accounts
@Published private(set) var Accounts: Array<AccountManagerEntry> = []
// init creates a new AccountManager, reading saved account data from AppStorage
init() {
self.Accounts = AccountManager.readFromRaw()
}
// readFromRaw reads the current accounts from the stored data and transforms it to a handy Account Array
static private func readFromRaw() -> Array<AccountManagerEntry> {
@AppStorage("accounts") var rawAccs = Data()
do {
let simpleAccs = try JSONDecoder().decode([Account].self, from: rawAccs)
let richAccs = simpleAccs.map({ AccountManagerEntry($0) })
return richAccs
} catch {
print("Error parsing stored accounts from JSON: \(error)")
return []
}
}
// writeToRaw writes the current account data to the AppStorage
private func writeToRaw() {
@AppStorage("accounts") var rawAccs = Data()
do {
let simpleAccs = Accounts.map({ $0.GetAccount() })
rawAccs = try JSONEncoder().encode(simpleAccs)
} catch {
print("Error writing current accounts to JSON: \(error)")
}
}
// CurrentAccount returns the currently selected account if there is one
func CurrentAccount() -> AccountManagerEntry? {
if self.Accounts.count > 0 {
return self.Accounts.first
}
return nil
}
// AddAccount adds the given account if it doesn't exist yet
func AddAccount(_ account: Account) {
if IndexOfAccount(account) == nil {
Accounts.append(AccountManagerEntry(account))
writeToRaw()
}
}
// IndexOfAccount returns the index of the specified account identified by address and user ID
func IndexOfAccount(_ account: Account) -> Int? {
return Accounts.firstIndex(where: {
$0.GetAccount().GetAddress() == account.GetAddress() &&
$0.GetAccount().GetUserID() == account.GetUserID()
})
}
// RemoveAccount removes the account specified by address and user ID of the given Account, if present
func RemoveAccount(_ account: Account) {
if IndexOfAccount(account) != nil {
Accounts.remove(at: IndexOfAccount(account)!)
writeToRaw()
}
}
// RemoveAccount removes the account specified by address and user ID of the given AccountManagerEntry, if present
func RemoveAccount(_ account: AccountManagerEntry) {
RemoveAccount(account.GetAccount())
}
}
+79
View File
@@ -0,0 +1,79 @@
//
// MeteClient.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
import Foundation
enum MeteClientError: Error {
case StatusCodeLooksBad(explanation: String)
}
/// **MeteClient** is the client-side implementation of the Space Market API. Pure Swift, no platform-specific frameworks used.
class MeteClient {
private let address: String
private let userID: Int
convenience init(account: Account) {
self.init(address: account.GetAddress(), userID: account.GetUserID())
}
init(address: String, userID: Int) {
self.address = address
self.userID = userID
}
// GetDrinks returns the list of drinks available at the backend
func GetDrinks() async throws -> Array<Drink> {
// Send HTTP request to the drinks endpoint
let url = URL(string: "https://\(address)/api/v1/drinks")
let (data, _) = try await URLSession.shared.data(from: url!)
// Decode JSON to array of drinks
let decoder = JSONDecoder()
let drinks = try decoder.decode([Drink].self, from: data)
return drinks
}
// GetUser returns the user object for the given ID
func GetUser() async throws -> User {
// Send HTTP request to the user endpoint
let url = URL(string: "https://\(address)/api/v1/users/\(userID).json")
let (data, _) = try await URLSession.shared.data(from: url!)
// Decode JSON to array of drinks
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
return user
}
// GetUsers returns all users registered at the backend
func GetUsers() async throws -> Array<User> {
// Send HTTP request to the user endpoint
let url = URL(string: "https://\(address)/api/v1/users.json")
let (data, _) = try await URLSession.shared.data(from: url!)
// Decode JSON to array of drinks
let decoder = JSONDecoder()
let users = try decoder.decode([User].self, from: data)
return users
}
// BookDrink subtracts the recommended price off the balance from the user given by ID
func BookDrink(_ drinkID: Int) async throws {
// Send HTTP request to the user endpoint
let url = URL(string: "https://\(address)/api/v1/users/\(userID)/buy.json?drink=\(drinkID)")
let (_, response) = try await URLSession.shared.data(from: url!)
// Check status code
let statusCode = (response as? HTTPURLResponse)?.statusCode
if statusCode != 204 {
throw MeteClientError.StatusCodeLooksBad(explanation: "Failed to book drink \(drinkID) for user \(userID)@\(address): HTTP status code \(statusCode) received for \(url)")
}
}
}
+12 -6
View File
@@ -13,13 +13,13 @@ enum WCMeteriosMessageTypes: String {
case BookDrink
}
class WCManager: NSObject, WCSessionDelegate {
@AppStorage("meteHostAddr") private var meteHostAddr = ""
@AppStorage("meteUserID") private var meteUserID = -1
/// **WCManager** managed the Meterios-specific communication between an iOS and an watchOS device
class WCManager: NSObject, WCSessionDelegate, ObservableObject {
private var accountManager: AccountManager
private let session: WCSession = WCSession.default
override init() {
self.accountManager = AccountManager.default
super.init()
if (WCSession.isSupported()) {
@@ -39,12 +39,18 @@ class WCManager: NSObject, WCSessionDelegate {
// As Meterios currently only needs messages from watchOS to iOS and responses back to the watch, this function currently only implements iOS-related responses.
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
#if os(iOS)
// Prevent crashes when WCSession is received before AccountManager is ready
if accountManager.CurrentAccount() == nil {
print("Request encountered in WCSession, but AccountManager is not ready.")
return
}
// Check for the different possible message types
switch message["request"] as! String {
case WCMeteriosMessageTypes.DrinkList.rawValue:
// Request for a list of available drinks
Task {
let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr)
let drinks = try await accountManager.CurrentAccount()!.GetClient().GetDrinks()
let favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active })
let availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active })
// note that drinks which are not active/available are not sent to the watch - the screen is too small to waste pixels.
@@ -64,7 +70,7 @@ class WCManager: NSObject, WCSessionDelegate {
do {
// Book
let drinkID = message["drinkID"] as! Int
try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID)
try await accountManager.CurrentAccount()!.GetClient().BookDrink(drinkID)
replyHandler([
"salut": SalutManager.getSalute()
])
+24
View File
@@ -0,0 +1,24 @@
//
// Account.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
struct Account: Encodable, Decodable {
private let address: String
private let userID: Int
init(address: String, userID: Int) {
self.address = address
self.userID = userID
}
func GetAddress() -> String {
return address
}
func GetUserID() -> Int {
return userID
}
}
+24
View File
@@ -0,0 +1,24 @@
//
// AccountManagerEntry.swift
// meterios
//
// (c) 2025 Martin "maride" Dessauer
//
struct AccountManagerEntry {
private let account: Account
private let client: MeteClient
init(_ account: Account) {
self.account = account
self.client = MeteClient(account: account)
}
func GetAccount() -> Account {
return account
}
func GetClient() -> MeteClient {
return client
}
}