Replace lazy AppStorage calls with AccountManager
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user