From 7582182196ae030c4374527fc9bdd49c05c7be29 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:30:09 +0200 Subject: [PATCH 01/17] model v1 calls the column lastModifiedLocal fixes #759 --- .../BeeminderModel.xcdatamodel/contents | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents index bf3dece92..2edccf482 100644 --- a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents +++ b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents @@ -4,7 +4,7 @@ - + @@ -20,7 +20,7 @@ - + @@ -45,10 +45,10 @@ - + - \ No newline at end of file + From 5054ff1861f0088c2c477658524b1d24cb8d8afa Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:33:17 +0200 Subject: [PATCH 02/17] restore builds/iosbeta/54's v1 model v2 already contains the migration path - renaming lastModifiedLocal to lastUpdatedLocal and declaring the new columns dueBy and updatedAt --- .../BeeminderModel.xcdatamodel/contents | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents index 2edccf482..cc994d00a 100644 --- a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents +++ b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -14,7 +14,6 @@ - @@ -47,8 +46,7 @@ - - + \ No newline at end of file From c192009295b402073c079507689b3b2cca5f08a5 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:45:25 +0200 Subject: [PATCH 03/17] if let try? --- BeeKit/Managers/GoalManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index 6d93a1fc4..f5ca6989e 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -160,7 +160,7 @@ import SwiftyJSON let request = NSFetchRequest(entityName: "Goal") request.predicate = NSPredicate(format: "id == %@", goalId) // TODO: Better error handling of failure here? - if let existingGoal = try! modelContext.fetch(request).first { + if let existingGoal = try? modelContext.fetch(request).first { existingGoal.updateToMatch(json: goalJSON) } else { let _ = Goal(context: modelContext, owner: user, json: goalJSON) From d1270bddefbcb5dec27fe18b30b265b9d483d6b8 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:46:10 +0200 Subject: [PATCH 04/17] lint --- BeeKit/Managers/GoalManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index f5ca6989e..a766fd002 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -163,7 +163,7 @@ import SwiftyJSON if let existingGoal = try? modelContext.fetch(request).first { existingGoal.updateToMatch(json: goalJSON) } else { - let _ = Goal(context: modelContext, owner: user, json: goalJSON) + _ = Goal(context: modelContext, owner: user, json: goalJSON) } } From a85b231185a9a18ea1506fb9482290b67df5c436 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:03:36 +0200 Subject: [PATCH 05/17] 2 failing tests the switch to b54 v1 of the beemindermodel meant that the migration tests broken since they were still setting properties which the restored v1 of the db no longer had with the renamingIdentifier in place, coredata can handle the light migrations itself --- BeeKitTests/MigrationTests.swift | 67 +++++++++++++++----------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/BeeKitTests/MigrationTests.swift b/BeeKitTests/MigrationTests.swift index 4d417060e..f10c887fb 100644 --- a/BeeKitTests/MigrationTests.swift +++ b/BeeKitTests/MigrationTests.swift @@ -5,9 +5,9 @@ import XCTest class MigrationTests: XCTestCase { private struct TestData { - static let userLastModified = Date(timeIntervalSince1970: 1_600_000_000) - static let goalLastModified = Date(timeIntervalSince1970: 1_610_000_000) - static let dataPointLastModified = Date(timeIntervalSince1970: 1_620_000_000) + static let userLastModified = Date(timeIntervalSince1970: 0) + static let goalLastModified = Date(timeIntervalSince1970: 0) + static let dataPointLastModified = Date(timeIntervalSince1970: 0) } override func tearDown() { super.tearDown() @@ -23,7 +23,7 @@ class MigrationTests: XCTestCase { } } // Creates a CoreData store with the old model version (v1) - private func createStoreWithOldModel() -> URL { + private func createStoreWithb54Model() -> URL { let storeURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent( "TestMigration_\(UUID().uuidString).sqlite" ) @@ -52,8 +52,6 @@ class MigrationTests: XCTestCase { user.setValue("testuser", forKey: "username") user.setValue("America/Los_Angeles", forKey: "timezone") user.setValue(false, forKey: "deadbeat") - user.setValue(Date(), forKey: "updatedAt") - user.setValue(TestData.userLastModified, forKey: "lastModifiedLocal") // Create goal with minimal required fields let goal = NSEntityDescription.insertNewObject(forEntityName: "Goal", into: context) @@ -68,15 +66,13 @@ class MigrationTests: XCTestCase { goal.setValue(0, forKey: field) } for field in ["hhmmFormat", "queued", "todayta", "useDefaults", "won"] { goal.setValue(false, forKey: field) } - goal.setValue(DueByDictionary(), forKey: "dueBy") - goal.setValue(TestData.goalLastModified, forKey: "lastModifiedLocal") goal.setValue(user, forKey: "owner") // Create datapoint let dataPoint = NSEntityDescription.insertNewObject(forEntityName: "DataPoint", into: context) dataPoint.setValue("dp1", forKey: "id") dataPoint.setValue("20230101", forKey: "daystampRaw") dataPoint.setValue(NSDecimalNumber(value: 1.0), forKey: "value") - dataPoint.setValue(TestData.dataPointLastModified, forKey: "lastModifiedLocal") + dataPoint.setValue(goal, forKey: "goal") try context.save() return storeURL @@ -89,7 +85,7 @@ class MigrationTests: XCTestCase { func testLastUpdatedLocalMigration() throws { DueByTableValueTransformer.register() - let storeURL = createStoreWithOldModel() + let storeURL = createStoreWithb54Model() let container = BeeminderPersistentContainer(name: "BeeminderModel") let description = NSPersistentStoreDescription(url: storeURL) container.persistentStoreDescriptions = [description] @@ -106,43 +102,43 @@ class MigrationTests: XCTestCase { let userRequest = NSFetchRequest(entityName: "User") let users = try context.fetch(userRequest) XCTAssertEqual(users.count, 1, "Should have one user after migration") - if let user = users.first { - XCTAssertEqual( - user.lastUpdatedLocal.timeIntervalSince1970, - TestData.userLastModified.timeIntervalSince1970, - accuracy: 0.001, - "User date value should be preserved during migration" - ) - } + let user = try XCTUnwrap(users.first) + XCTAssertEqual( + user.lastUpdatedLocal.timeIntervalSince1970, + TestData.userLastModified.timeIntervalSince1970, + accuracy: 0.001, + "User date value should be preserved during migration" + ) + // Migration on Goal let goalRequest = NSFetchRequest(entityName: "Goal") let goals = try context.fetch(goalRequest) XCTAssertEqual(goals.count, 1, "Should have one goal after migration") - if let goal = goals.first { - XCTAssertEqual( - goal.lastUpdatedLocal.timeIntervalSince1970, - TestData.goalLastModified.timeIntervalSince1970, - accuracy: 0.001, - "Goal date value should be preserved during migration" - ) - } + let goal = try XCTUnwrap(goals.first) + XCTAssertEqual( + goal.lastUpdatedLocal.timeIntervalSince1970, + TestData.goalLastModified.timeIntervalSince1970, + accuracy: 0.001, + "Goal date value should be preserved during migration" + ) + // Migration on DataPoint let dataPointRequest = NSFetchRequest(entityName: "DataPoint") let dataPoints = try context.fetch(dataPointRequest) XCTAssertEqual(dataPoints.count, 1, "Should have one data point after migration") - if let dataPoint = dataPoints.first { - XCTAssertEqual( - dataPoint.lastUpdatedLocal.timeIntervalSince1970, - TestData.dataPointLastModified.timeIntervalSince1970, - accuracy: 0.001, - "DataPoint date value should be preserved during migration" - ) - } + let dataPoint = try XCTUnwrap(dataPoints.first) + XCTAssertEqual( + dataPoint.lastUpdatedLocal.timeIntervalSince1970, + TestData.dataPointLastModified.timeIntervalSince1970, + accuracy: 0.001, + "DataPoint date value should be preserved during migration" + ) + } func testAutodataConfigMigration() throws { DueByTableValueTransformer.register() - let storeURL = createStoreWithOldModel() + let storeURL = createStoreWithb54Model() let container = BeeminderPersistentContainer(name: "BeeminderModel") let description = NSPersistentStoreDescription(url: storeURL) container.persistentStoreDescriptions = [description] @@ -163,3 +159,4 @@ class MigrationTests: XCTestCase { XCTAssertTrue(goal.autodataConfig.isEmpty, "autodataConfig should be empty dict for migrated goals") } } + From 3d094eeec6d1f882f7ac20ee43140ae6739ab5f8 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:08:33 +0200 Subject: [PATCH 06/17] formatting --- BeeKitTests/MigrationTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/BeeKitTests/MigrationTests.swift b/BeeKitTests/MigrationTests.swift index f10c887fb..ed62bca14 100644 --- a/BeeKitTests/MigrationTests.swift +++ b/BeeKitTests/MigrationTests.swift @@ -109,7 +109,6 @@ class MigrationTests: XCTestCase { accuracy: 0.001, "User date value should be preserved during migration" ) - // Migration on Goal let goalRequest = NSFetchRequest(entityName: "Goal") let goals = try context.fetch(goalRequest) @@ -159,4 +158,3 @@ class MigrationTests: XCTestCase { XCTAssertTrue(goal.autodataConfig.isEmpty, "autodataConfig should be empty dict for migrated goals") } } - From 9224387e2756bd8431162aaa8c7e2db323eb2934 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:42:15 +0200 Subject: [PATCH 07/17] performance tweak using static cached property with lazy initialization --- BeeSwift/Components/UI/UIFontExtension.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/BeeSwift/Components/UI/UIFontExtension.swift b/BeeSwift/Components/UI/UIFontExtension.swift index b41ee1951..bbcd93ce3 100644 --- a/BeeSwift/Components/UI/UIFontExtension.swift +++ b/BeeSwift/Components/UI/UIFontExtension.swift @@ -11,10 +11,18 @@ import UIKit extension UIFont { public struct beeminder { - public static var defaultFont: UIFont { return defaultFontLight } - public static var defaultFontLight: UIFont { return UIFont(name: "Avenir-Light", size: 18)! } - public static var defaultFontHeavy: UIFont { return UIFont(name: "Avenir-Heavy", size: 18)! } - public static var defaultBoldFont: UIFont { return UIFont(name: "Avenir-Black", size: 18)! } - public static var defaultFontPlain: UIFont { return UIFont(name: "Avenir", size: 18)! } + public static var defaultFont: UIFont = { defaultFontLight }() + public static var defaultFontLight: UIFont = { + UIFont(name: "Avenir-Light", size: 18) ?? .systemFont(ofSize: 18, weight: .light) + }() + public static var defaultFontHeavy: UIFont = { + UIFont(name: "Avenir-Heavy", size: 18) ?? .systemFont(ofSize: 18, weight: .heavy) + }() + public static var defaultBoldFont: UIFont = { + UIFont(name: "Avenir-Black", size: 18) ?? .systemFont(ofSize: 18, weight: .black) + }() + public static var defaultFontPlain: UIFont = { + UIFont(name: "Avenir", size: 18) ?? .systemFont(ofSize: 18, weight: .regular) + }() } } From c51130f196072b9b9e85e5a0dce1618b0be3c1c1 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:31:30 +0200 Subject: [PATCH 08/17] Revert "if let try?" This reverts commit 8d8c6dace66d686c5b6bdf1c1e5d8a8b84ed1985. --- BeeKit/Managers/GoalManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index a766fd002..8a8252d45 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -160,7 +160,7 @@ import SwiftyJSON let request = NSFetchRequest(entityName: "Goal") request.predicate = NSPredicate(format: "id == %@", goalId) // TODO: Better error handling of failure here? - if let existingGoal = try? modelContext.fetch(request).first { + if let existingGoal = try! modelContext.fetch(request).first { existingGoal.updateToMatch(json: goalJSON) } else { _ = Goal(context: modelContext, owner: user, json: goalJSON) From 84eb281585f930bba4bacecf3b856fb8fc362ba0 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:56:35 +0200 Subject: [PATCH 09/17] catching and logging modelContext.fetch in updateGoalsFromJson --- BeeKit/Managers/GoalManager.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index 8a8252d45..50620e1d8 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -159,11 +159,16 @@ import SwiftyJSON let goalId = goalJSON["id"].stringValue let request = NSFetchRequest(entityName: "Goal") request.predicate = NSPredicate(format: "id == %@", goalId) - // TODO: Better error handling of failure here? - if let existingGoal = try! modelContext.fetch(request).first { - existingGoal.updateToMatch(json: goalJSON) - } else { - _ = Goal(context: modelContext, owner: user, json: goalJSON) + + do { + if let existingGoal = try modelContext.fetch(request).first { + existingGoal.updateToMatch(json: goalJSON) + } else { + _ = Goal(context: modelContext, owner: user, json: goalJSON) + } + } catch { + logger.error("modelContext.fetch failed (for id: \(goalId)) with error: \(error)") + continue } } From 066744afcbc8969c1061346cf9e375d0f45c1b86 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:30:56 +0200 Subject: [PATCH 10/17] logging and letting errors in updateGoalsFromJson propagate --- BeeKit/Managers/GoalManager.swift | 41 ++++++++++++++----------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index 50620e1d8..0f9e09fc7 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -97,7 +97,7 @@ import SwiftyJSON let goalsToDelete = user.goals.filter { !allGoalIds.contains($0.id) } for goal in goalsToDelete { modelContext.delete(goal) } } - updateGoalsFromJson(goalResponse) + try updateGoalsFromJson(goalResponse) } /// Perform an incremental refresh of goals for regular updates private func refreshGoalsIncremental(user: User) async throws { @@ -118,57 +118,54 @@ import SwiftyJSON let deletedGoalIds = Set(deletedGoals.arrayValue.map { $0["id"].stringValue }) let goalsToDelete = user.goals.filter { deletedGoalIds.contains($0.id) } for goal in goalsToDelete { modelContext.delete(goal) } - updateGoalsFromJson(goalResponse) + try updateGoalsFromJson(goalResponse) // Update lastUpdatedLocal for all goals, even those not in response let now = Date() for goal in user.goals { goal.lastUpdatedLocal = now } } - public func refreshGoal(_ goalID: NSManagedObjectID) async throws { let goal = try modelContext.existingObject(with: goalID) as! Goal - let responseObject = try await requestManager.get( url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)", parameters: ["datapoints_count": "5", "emaciated": "true"] ) let goalJSON = JSON(responseObject!) - // The goal may have changed during the network operation, reload latest version modelContext.refresh(goal, mergeChanges: false) goal.updateToMatch(json: goalJSON) - try modelContext.save() - await performPostGoalUpdateBookkeeping() } - public func forceAutodataRefresh(_ goal: Goal) async throws { let _ = try await requestManager.get( url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)/refresh_graph.json" ) } - private func updateGoalsFromJson(_ responseJSON: JSON) { - guard let responseGoals = responseJSON.array else { return } + private func updateGoalsFromJson(_ responseJSON: JSON) throws { + guard let responseGoals = responseJSON.array else { + logger.error("responseJSON apparently not array") + return + } - // The user may have logged out while waiting for the data, so ignore if so - guard let user = self.currentUserManager.user(context: modelContext) else { return } + guard let user = self.currentUserManager.user(context: modelContext) else { + logger.info("The user may have logged out while waiting for the data, so ignore if so") + return + } // Create and update existing goals for goalJSON in responseGoals { - let goalId = goalJSON["id"].stringValue + guard let goalId = goalJSON["id"].string else { + logger.error("goalJSON missing id") + continue + } let request = NSFetchRequest(entityName: "Goal") request.predicate = NSPredicate(format: "id == %@", goalId) - do { - if let existingGoal = try modelContext.fetch(request).first { - existingGoal.updateToMatch(json: goalJSON) - } else { - _ = Goal(context: modelContext, owner: user, json: goalJSON) - } - } catch { - logger.error("modelContext.fetch failed (for id: \(goalId)) with error: \(error)") - continue + if let existingGoal = try modelContext.fetch(request).first { + existingGoal.updateToMatch(json: goalJSON) + } else { + _ = Goal(context: modelContext, owner: user, json: goalJSON) } } From 1f8b1c97be2262fb0ad204e9d65ed1d82e8a7b93 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:19:47 +0200 Subject: [PATCH 11/17] rather than force unwrapping, naming errors in GoalManager mostly to facilitate troubleshooting --- BeeKit/Managers/GoalManager.swift | 49 +++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index 0f9e09fc7..c048ca148 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -82,10 +82,15 @@ import SwiftyJSON logger.notice("Goals unknown, doing full fetch") // We must fetch the user object first, and then fetch goals afterwards, to guarantee User.updated_at is // a safe timestamp for future fetches without losing data - let userResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}.json")!) - let goalResponse = JSON( - try await requestManager.get(url: "api/v1/users/{username}/goals.json", parameters: ["emaciated": "true"])! - ) + guard let getUser = try await requestManager.get(url: "api/v1/users/{username}.json") else { + throw GoalManagerError.getUserFailed + } + let userResponse = JSON(getUser) + + guard let getGoals = try await requestManager.get(url: "api/v1/users/{username}/goals.json", parameters: ["emaciated": "true"]) else { + throw GoalManagerError.getGoalsFailed + } + let goalResponse = JSON(getGoals) // The user may have logged out during the network operation. If so we have nothing to do modelContext.refreshAllObjects() @@ -102,12 +107,17 @@ import SwiftyJSON /// Perform an incremental refresh of goals for regular updates private func refreshGoalsIncremental(user: User) async throws { logger.notice("Doing incremental update since \(user.updatedAt, privacy: .public)") - let userResponse = JSON( - try await requestManager.get( + + guard let getUser = try await requestManager.get( url: "api/v1/users/{username}.json", parameters: ["diff_since": user.updatedAt.timeIntervalSince1970 + 1, "emaciated": "true"] - )! - ) + ) + else { + throw GoalManagerError.getUserFailed + } + + let userResponse = JSON(getUser) + let goalResponse = userResponse["goals"] let deletedGoals = userResponse["deleted_goals"] // The user may have logged out during the network operation. If so we have nothing to do @@ -124,12 +134,18 @@ import SwiftyJSON for goal in user.goals { goal.lastUpdatedLocal = now } } public func refreshGoal(_ goalID: NSManagedObjectID) async throws { - let goal = try modelContext.existingObject(with: goalID) as! Goal - let responseObject = try await requestManager.get( + guard + let goal = try modelContext.existingObject(with: goalID) as? Goal + else { + throw GoalManagerError.refreshGoalFailed(goalID: goalID, reason: "goal not found") + } + guard let responseObject = try await requestManager.get( url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)", parameters: ["datapoints_count": "5", "emaciated": "true"] - ) - let goalJSON = JSON(responseObject!) + ) else { + throw GoalManagerError.getGoalFailed(goalname: goal.slug, goalID: goal.id) + } + let goalJSON = JSON(responseObject) // The goal may have changed during the network operation, reload latest version modelContext.refresh(goal, mergeChanges: false) goal.updateToMatch(json: goalJSON) @@ -224,3 +240,12 @@ import SwiftyJSON // TODO: Delete from CoreData } } + +private extension GoalManager { + enum GoalManagerError: Error { + case getUserFailed + case getGoalsFailed + case getGoalFailed(goalname: String, goalID: String) + case refreshGoalFailed(goalID: NSManagedObjectID, reason: String) + } +} From 26b60c58ac9d3e6151aa2d3c2d479b14a3bf353a Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:20:25 +0200 Subject: [PATCH 12/17] a goal's owner --- BeeKit/Managers/GoalManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index c048ca148..48d02dd4d 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -140,7 +140,7 @@ import SwiftyJSON throw GoalManagerError.refreshGoalFailed(goalID: goalID, reason: "goal not found") } guard let responseObject = try await requestManager.get( - url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)", + url: "/api/v1/users/\(goal.owner.username)/goals/\(goal.slug)", parameters: ["datapoints_count": "5", "emaciated": "true"] ) else { throw GoalManagerError.getGoalFailed(goalname: goal.slug, goalID: goal.id) @@ -154,7 +154,7 @@ import SwiftyJSON } public func forceAutodataRefresh(_ goal: Goal) async throws { let _ = try await requestManager.get( - url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)/refresh_graph.json" + url: "/api/v1/users/\(goal.owner.username)/goals/\(goal.slug)/refresh_graph.json" ) } From f792f320a38d60524531079ee631810b855c79e3 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:26:21 +0200 Subject: [PATCH 13/17] named VersionError rather than Fatal Error found nil while unwrapping Optional --- BeeKit/Managers/VersionManager.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BeeKit/Managers/VersionManager.swift b/BeeKit/Managers/VersionManager.swift index 9a48371f8..b2a8b1b1f 100644 --- a/BeeKit/Managers/VersionManager.swift +++ b/BeeKit/Managers/VersionManager.swift @@ -56,9 +56,11 @@ public class VersionManager { return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String } private func checkIfUpdateRequired() async throws -> Bool { - let responseJSON = try await requestManager.get(url: "api/private/app_versions.json") + guard let responseJSON = try await requestManager.get(url: "api/private/app_versions.json") else { + throw VersionError.invalidServerResponse + } - guard let response = JSON(responseJSON!).dictionary else { throw VersionError.invalidServerResponse } + guard let response = JSON(responseJSON).dictionary else { throw VersionError.invalidServerResponse } guard let minVersion = response["min_ios"]?.number?.decimalValue else { throw VersionError.noMinimumVersion } minRequiredVersion = "\(minVersion)" From 735a5d00f687e8dde39c5bc9be147b45af4222ce Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:27:08 +0200 Subject: [PATCH 14/17] clean code --- BeeKit/Managers/VersionManager.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BeeKit/Managers/VersionManager.swift b/BeeKit/Managers/VersionManager.swift index b2a8b1b1f..1d8c43058 100644 --- a/BeeKit/Managers/VersionManager.swift +++ b/BeeKit/Managers/VersionManager.swift @@ -56,11 +56,13 @@ public class VersionManager { return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String } private func checkIfUpdateRequired() async throws -> Bool { - guard let responseJSON = try await requestManager.get(url: "api/private/app_versions.json") else { + guard + let responseJSON = try await requestManager.get(url: "api/private/app_versions.json"), + let response = JSON(responseJSON).dictionary + else { throw VersionError.invalidServerResponse } - guard let response = JSON(responseJSON).dictionary else { throw VersionError.invalidServerResponse } guard let minVersion = response["min_ios"]?.number?.decimalValue else { throw VersionError.noMinimumVersion } minRequiredVersion = "\(minVersion)" From d49f548b5267812d6002746541bc5c9d08d29eed Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:38:42 +0200 Subject: [PATCH 15/17] descriptions of GoalManagerErrors missing --- BeeKit/Managers/GoalManager.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index 48d02dd4d..9c1540cfe 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -249,3 +249,19 @@ private extension GoalManager { case refreshGoalFailed(goalID: NSManagedObjectID, reason: String) } } + +extension GoalManager.GoalManagerError: LocalizedError { + public var errorDescription: String? { + switch self { + case .getGoalsFailed: + return NSLocalizedString("Failed to get goals", comment: "getGoalsFailed") + case .getUserFailed: + return NSLocalizedString("Failed to get user", comment: "getUserFailed") + case .getGoalFailed(let goalname, let goalID): + return NSLocalizedString("Failed to get goal: \(goalname) with id: \(goalID)", comment: "getGoalFailed") + case .refreshGoalFailed(let goalID, let reason): + return NSLocalizedString("Failed to refresh goal: \(goalID) because: \(reason)", comment: "refreshGoalFailed") + } + } +} + From b8a8f6af5219794d3cb96d1753271cc906201c4c Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:47:40 +0200 Subject: [PATCH 16/17] one force unwrap down --- BeeSwift/Components/DatapointValueAccessory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BeeSwift/Components/DatapointValueAccessory.swift b/BeeSwift/Components/DatapointValueAccessory.swift index e19e77c01..d1379e2c4 100644 --- a/BeeSwift/Components/DatapointValueAccessory.swift +++ b/BeeSwift/Components/DatapointValueAccessory.swift @@ -85,7 +85,7 @@ class DatapointValueAccessory: UIInputView { @objc func colonButtonPressed() { guard let valueField = self.valueField else { return } - valueField.text = "\(valueField.text!):" + valueField.text = valueField.text?.appending(":") } } From 2bb136fd5bf907a0c9ed3a5e81e6577abb19f705 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:06:46 +0200 Subject: [PATCH 17/17] swift-format --recursive --in-place . --- BeeKit/Managers/GoalManager.swift | 47 ++++++++++++---------------- BeeKit/Managers/VersionManager.swift | 7 ++--- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/BeeKit/Managers/GoalManager.swift b/BeeKit/Managers/GoalManager.swift index 9c1540cfe..89bccb01e 100644 --- a/BeeKit/Managers/GoalManager.swift +++ b/BeeKit/Managers/GoalManager.swift @@ -86,10 +86,12 @@ import SwiftyJSON throw GoalManagerError.getUserFailed } let userResponse = JSON(getUser) - - guard let getGoals = try await requestManager.get(url: "api/v1/users/{username}/goals.json", parameters: ["emaciated": "true"]) else { - throw GoalManagerError.getGoalsFailed - } + guard + let getGoals = try await requestManager.get( + url: "api/v1/users/{username}/goals.json", + parameters: ["emaciated": "true"] + ) + else { throw GoalManagerError.getGoalsFailed } let goalResponse = JSON(getGoals) // The user may have logged out during the network operation. If so we have nothing to do @@ -107,17 +109,13 @@ import SwiftyJSON /// Perform an incremental refresh of goals for regular updates private func refreshGoalsIncremental(user: User) async throws { logger.notice("Doing incremental update since \(user.updatedAt, privacy: .public)") - - guard let getUser = try await requestManager.get( + guard + let getUser = try await requestManager.get( url: "api/v1/users/{username}.json", parameters: ["diff_since": user.updatedAt.timeIntervalSince1970 + 1, "emaciated": "true"] ) - else { - throw GoalManagerError.getUserFailed - } - + else { throw GoalManagerError.getUserFailed } let userResponse = JSON(getUser) - let goalResponse = userResponse["goals"] let deletedGoals = userResponse["deleted_goals"] // The user may have logged out during the network operation. If so we have nothing to do @@ -134,17 +132,15 @@ import SwiftyJSON for goal in user.goals { goal.lastUpdatedLocal = now } } public func refreshGoal(_ goalID: NSManagedObjectID) async throws { - guard - let goal = try modelContext.existingObject(with: goalID) as? Goal - else { + guard let goal = try modelContext.existingObject(with: goalID) as? Goal else { throw GoalManagerError.refreshGoalFailed(goalID: goalID, reason: "goal not found") } - guard let responseObject = try await requestManager.get( - url: "/api/v1/users/\(goal.owner.username)/goals/\(goal.slug)", - parameters: ["datapoints_count": "5", "emaciated": "true"] - ) else { - throw GoalManagerError.getGoalFailed(goalname: goal.slug, goalID: goal.id) - } + guard + let responseObject = try await requestManager.get( + url: "/api/v1/users/\(goal.owner.username)/goals/\(goal.slug)", + parameters: ["datapoints_count": "5", "emaciated": "true"] + ) + else { throw GoalManagerError.getGoalFailed(goalname: goal.slug, goalID: goal.id) } let goalJSON = JSON(responseObject) // The goal may have changed during the network operation, reload latest version modelContext.refresh(goal, mergeChanges: false) @@ -241,8 +237,8 @@ import SwiftyJSON } } -private extension GoalManager { - enum GoalManagerError: Error { +extension GoalManager { + fileprivate enum GoalManagerError: Error { case getUserFailed case getGoalsFailed case getGoalFailed(goalname: String, goalID: String) @@ -253,10 +249,8 @@ private extension GoalManager { extension GoalManager.GoalManagerError: LocalizedError { public var errorDescription: String? { switch self { - case .getGoalsFailed: - return NSLocalizedString("Failed to get goals", comment: "getGoalsFailed") - case .getUserFailed: - return NSLocalizedString("Failed to get user", comment: "getUserFailed") + case .getGoalsFailed: return NSLocalizedString("Failed to get goals", comment: "getGoalsFailed") + case .getUserFailed: return NSLocalizedString("Failed to get user", comment: "getUserFailed") case .getGoalFailed(let goalname, let goalID): return NSLocalizedString("Failed to get goal: \(goalname) with id: \(goalID)", comment: "getGoalFailed") case .refreshGoalFailed(let goalID, let reason): @@ -264,4 +258,3 @@ extension GoalManager.GoalManagerError: LocalizedError { } } } - diff --git a/BeeKit/Managers/VersionManager.swift b/BeeKit/Managers/VersionManager.swift index 1d8c43058..e809a37f3 100644 --- a/BeeKit/Managers/VersionManager.swift +++ b/BeeKit/Managers/VersionManager.swift @@ -56,12 +56,9 @@ public class VersionManager { return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String } private func checkIfUpdateRequired() async throws -> Bool { - guard - let responseJSON = try await requestManager.get(url: "api/private/app_versions.json"), + guard let responseJSON = try await requestManager.get(url: "api/private/app_versions.json"), let response = JSON(responseJSON).dictionary - else { - throw VersionError.invalidServerResponse - } + else { throw VersionError.invalidServerResponse } guard let minVersion = response["min_ios"]?.number?.decimalValue else { throw VersionError.noMinimumVersion } minRequiredVersion = "\(minVersion)"