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)"