From 093c309cd0436b16c98dd51f93bc1fb7d0edaa4c Mon Sep 17 00:00:00 2001
From: krugerk <4656811+krugerk@users.noreply.github.com>
Date: Mon, 6 Jan 2025 18:39:54 +0100
Subject: [PATCH 1/3] goalvc shows also a goal's rate
---
.../BeeminderModel.xcdatamodel/contents | 3 +++
BeeKit/Model/Goal.swift | 15 ++++++++++++++-
BeeSwift/GoalViewController.swift | 17 ++++++++++++++++-
3 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
index cc994d00..5c20549d 100644
--- a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
+++ b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
@@ -14,6 +14,7 @@
+
@@ -25,6 +26,8 @@
+
+
diff --git a/BeeKit/Model/Goal.swift b/BeeKit/Model/Goal.swift
index 524b6edd..58d9b0e4 100644
--- a/BeeKit/Model/Goal.swift
+++ b/BeeKit/Model/Goal.swift
@@ -51,6 +51,15 @@ public class Goal: NSManagedObject {
@NSManaged public var won: Bool
/// The label for the y-axis of the graph. E.g., "Cumulative total hours".
@NSManaged public var yAxis: String
+
+ /// Goal units, like "hours" or "pushups" or "pages".
+ @NSManaged public var goalUnits: String
+
+ /// Rate units. One of y, m, w, d, h indicating that the rate of the bright red line is yearly, monthly, weekly, daily, or hourly.
+ @NSManaged public var rateUnits: String
+
+ /// The slope of the (final section of the) bright red line. You must also consider runits to fully specify the rate. NOTE: this may be null
+ @NSManaged public var rate: Double
@NSManaged public var recentData: Set
@@ -171,7 +180,11 @@ public class Goal: NSManagedObject {
self.useDefaults = json["use_defaults"].boolValue
self.won = json["won"].boolValue
self.yAxis = json["yaxis"].stringValue
-
+
+ self.goalUnits = json["gunits"].stringValue
+ self.rateUnits = json["runits"].stringValue
+ self.rate = json["rate"].doubleValue
+
// Replace recent data with results from server
// Note at present this leaks data points in the main db. This is probably fine for now
let newRecentData = Set(json["recent_data"].arrayValue.map {
diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift
index c5fd2d0a..6de52e8b 100644
--- a/BeeSwift/GoalViewController.swift
+++ b/BeeSwift/GoalViewController.swift
@@ -40,6 +40,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
fileprivate var submitButton = BSButton()
fileprivate let headerWidth = Double(1.0/3.0)
fileprivate let viewGoalActivityType = "com.beeminder.viewGoal"
+ private let goalRateLabel = BSLabel()
// date corresponding to the datapoint to be created
private var date: Date = Date()
@@ -136,12 +137,24 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
make.right.equalTo(self.goalImageScrollView)
}
self.goalImageView.goal = self.goal
+
+ self.scrollView.addSubview(goalRateLabel)
+ self.goalRateLabel.snp.makeConstraints { make in
+ make.top.equalTo(self.goalImageScrollView.snp.bottom).offset(elementSpacing)
+ make.height.equalTo(Constants.defaultFontSize)
+ make.left.equalTo(self.goalImageScrollView).offset(sideMargin)
+ make.right.equalTo(self.goalImageScrollView).offset(-sideMargin)
+ }
+ self.goalRateLabel.textAlignment = .center
+ self.goalRateLabel.font = UIFont.preferredFont(forTextStyle: .footnote).withSize(Constants.defaultFontSize * 0.9)
+ self.goalRateLabel.textColor = .label.withAlphaComponent(0.8)
+
self.addChild(self.datapointTableController)
self.scrollView.addSubview(self.datapointTableController.view)
self.datapointTableController.delegate = self
self.datapointTableController.view.snp.makeConstraints { (make) -> Void in
- make.top.equalTo(self.goalImageScrollView.snp.bottom).offset(elementSpacing)
+ make.top.equalTo(self.goalRateLabel.snp.bottom).offset(elementSpacing)
make.left.equalTo(self.goalImageScrollView).offset(sideMargin)
make.right.equalTo(self.goalImageScrollView).offset(-sideMargin)
}
@@ -499,6 +512,8 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
self.datapointTableController.hhmmformat = goal.hhmmFormat
self.datapointTableController.datapoints = goal.recentData.sorted(by: {$0.updatedAt < $1.updatedAt})
+ self.goalRateLabel.text = "\(goal.rate) \(goal.goalUnits) / \(goal.rateUnits)"
+
self.refreshCountdown()
self.updateLastUpdatedLabel()
}
From b7d855d67362b48f9c12f9e1f610486df9510cd1 Mon Sep 17 00:00:00 2001
From: krugerk <4656811+krugerk@users.noreply.github.com>
Date: Mon, 6 Jan 2025 19:25:22 +0100
Subject: [PATCH 2/3] a calculated rate can be found in mathishard
---
.../BeeminderModel.xcdatamodel/contents | 4 +-
BeeKit/Model/Goal.swift | 44 ++++++++++++++++---
BeeSwift/GoalViewController.swift | 19 +++++++-
3 files changed, 60 insertions(+), 7 deletions(-)
diff --git a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
index 5c20549d..aa5753d6 100644
--- a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
+++ b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
@@ -14,7 +14,10 @@
+
+
+
@@ -26,7 +29,6 @@
-
diff --git a/BeeKit/Model/Goal.swift b/BeeKit/Model/Goal.swift
index 58d9b0e4..a6c347b0 100644
--- a/BeeKit/Model/Goal.swift
+++ b/BeeKit/Model/Goal.swift
@@ -51,16 +51,20 @@ public class Goal: NSManagedObject {
@NSManaged public var won: Bool
/// The label for the y-axis of the graph. E.g., "Cumulative total hours".
@NSManaged public var yAxis: String
-
+
/// Goal units, like "hours" or "pushups" or "pages".
@NSManaged public var goalUnits: String
/// Rate units. One of y, m, w, d, h indicating that the rate of the bright red line is yearly, monthly, weekly, daily, or hourly.
@NSManaged public var rateUnits: String
- /// The slope of the (final section of the) bright red line. You must also consider runits to fully specify the rate. NOTE: this may be null
- @NSManaged public var rate: Double
-
+ /// The slope of the (final section of the) bright red line. You must also consider runits to fully specify the rate.
+ @NSManaged public var goalRateRaw: NSNumber?
+ /// Unix timestamp (in seconds) of the goal date.
+ @NSManaged public var goalDateRaw: NSNumber?
+ /// Goal value — the number the bright red line will eventually reach. E.g., 70 kilograms.
+ @NSManaged public var goalValueRaw: NSNumber?
+
@NSManaged public var recentData: Set
@objc(addRecentDataObject:)
@@ -183,7 +187,19 @@ public class Goal: NSManagedObject {
self.goalUnits = json["gunits"].stringValue
self.rateUnits = json["runits"].stringValue
- self.rate = json["rate"].doubleValue
+
+ // mathishard (array of 3 numbers): The goaldate, goalval, and rate — all filled in. (The commitment dial specifies 2 out of 3 and you can check this if you want Beeminder to do the math for you on inferring the third one.) Note: this field may be null if the goal is in an error state such that the graph image can't be generated.
+ let calculatedGoalDateGoalValueAndGoalRate = json["mathishard"].array
+ if let calculatedGoalDateGoalValueAndGoalRate, calculatedGoalDateGoalValueAndGoalRate.count == 3 {
+ self.goalDate = calculatedGoalDateGoalValueAndGoalRate[0].doubleValue
+ self.goalValue = calculatedGoalDateGoalValueAndGoalRate[1].doubleValue
+ self.goalRate = calculatedGoalDateGoalValueAndGoalRate[2].doubleValue
+ } else {
+ // logger.debug("mathishard array expected to have have 3 elements")
+ self.goalDateRaw = json["goaldate"].number
+ self.goalValueRaw = json["goalval"].number
+ self.goalRateRaw = json["rate"].number
+ }
// Replace recent data with results from server
// Note at present this leaks data points in the main db. This is probably fine for now
@@ -197,3 +213,21 @@ public class Goal: NSManagedObject {
lastModifiedLocal = Date()
}
}
+
+public extension Goal {
+ var goalDate: Double? {
+ get { goalDateRaw?.doubleValue }
+ set { goalDateRaw = newValue as NSNumber? }
+ }
+
+ var goalValue: Double? {
+ get { goalValueRaw?.doubleValue }
+ set { goalValueRaw = newValue as NSNumber? }
+ }
+
+ var goalRate: Double? {
+ get { goalRateRaw?.doubleValue }
+ set { goalRateRaw = newValue as NSNumber? }
+ }
+}
+
diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift
index 6de52e8b..bfcc9d5e 100644
--- a/BeeSwift/GoalViewController.swift
+++ b/BeeSwift/GoalViewController.swift
@@ -507,12 +507,29 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
try await ServiceLocator.goalManager.refreshGoal(self.goal.objectID)
updateInterfaceToMatchGoal()
}
+
+ private static var goalRateNumberFormatter: NumberFormatter {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .decimal
+ formatter.maximumFractionDigits = 3
+ return formatter
+ }
+
+ private var formattedGoalRate: String? {
+ guard let rate = goal.goalRate as NSNumber? else { return nil }
+
+ return Self.goalRateNumberFormatter.string(from: rate)
+ }
func updateInterfaceToMatchGoal() {
self.datapointTableController.hhmmformat = goal.hhmmFormat
self.datapointTableController.datapoints = goal.recentData.sorted(by: {$0.updatedAt < $1.updatedAt})
- self.goalRateLabel.text = "\(goal.rate) \(goal.goalUnits) / \(goal.rateUnits)"
+ self.goalRateLabel.isHidden = formattedGoalRate == nil
+ self.goalRateLabel.text = {
+ guard let formattedGoalRate else { return nil }
+ return "\(formattedGoalRate) \(goal.goalUnits) / \(goal.rateUnits)"
+ }()
self.refreshCountdown()
self.updateLastUpdatedLabel()
From d180182506194dcac020eefdd1d764fce65787e8 Mon Sep 17 00:00:00 2001
From: krugerk <4656811+krugerk@users.noreply.github.com>
Date: Tue, 7 Jan 2025 10:14:39 +0100
Subject: [PATCH 3/3] also showing the current rate on the gallery
---
BeeKit/GoalExtensions.swift | 20 ++++++++++++++++++++
BeeSwift/GoalCollectionViewCell.swift | 14 +++++++++++++-
BeeSwift/GoalViewController.swift | 20 ++------------------
3 files changed, 35 insertions(+), 19 deletions(-)
diff --git a/BeeKit/GoalExtensions.swift b/BeeKit/GoalExtensions.swift
index 27250451..aadb0979 100644
--- a/BeeKit/GoalExtensions.swift
+++ b/BeeKit/GoalExtensions.swift
@@ -86,3 +86,23 @@ extension Goal {
return candidateDatapoints.first?.value
}
}
+
+public extension Goal {
+ private static var goalRateNumberFormatter: NumberFormatter {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .decimal
+ formatter.maximumFractionDigits = 3
+ return formatter
+ }
+
+ private var formattedGoalRate: String? {
+ guard let rate = goalRate as NSNumber? else { return nil }
+
+ return Self.goalRateNumberFormatter.string(from: rate)
+ }
+
+ var currentRate: String? {
+ guard let formattedGoalRate else { return nil }
+ return "\(formattedGoalRate) \(goalUnits) / \(rateUnits)"
+ }
+}
diff --git a/BeeSwift/GoalCollectionViewCell.swift b/BeeSwift/GoalCollectionViewCell.swift
index daa65b18..448cdd48 100644
--- a/BeeSwift/GoalCollectionViewCell.swift
+++ b/BeeSwift/GoalCollectionViewCell.swift
@@ -16,6 +16,7 @@ class GoalCollectionViewCell: UICollectionViewCell {
let todaytaLabel :BSLabel = BSLabel()
let thumbnailImageView = GoalImageView(isThumbnail: true)
let safesumLabel :BSLabel = BSLabel()
+ let rateLabel = BSLabel()
let margin = 8
var goal: Goal? {
@@ -27,6 +28,7 @@ class GoalCollectionViewCell: UICollectionViewCell {
self.todaytaLabel.text = goal?.todayta == true ? "✓" : ""
self.safesumLabel.text = goal?.capitalSafesum()
self.safesumLabel.textColor = goal?.countdownColor ?? UIColor.Beeminder.gray
+ self.rateLabel.text = goal?.currentRate
}
}
@@ -38,6 +40,7 @@ class GoalCollectionViewCell: UICollectionViewCell {
self.contentView.addSubview(self.todaytaLabel)
self.contentView.addSubview(self.thumbnailImageView)
self.contentView.addSubview(self.safesumLabel)
+ self.contentView.addSubview(self.rateLabel)
self.contentView.backgroundColor = .systemBackground
self.slugLabel.font = UIFont.beeminder.defaultFontHeavy
@@ -78,7 +81,16 @@ class GoalCollectionViewCell: UICollectionViewCell {
self.safesumLabel.numberOfLines = 0
self.safesumLabel.snp.makeConstraints { (make) -> Void in
make.left.equalTo(self.thumbnailImageView.snp.right).offset(5)
- make.centerY.equalTo(self.thumbnailImageView.snp.centerY)
+ make.centerY.equalTo(self.thumbnailImageView.snp.centerY).offset(-8)
+ make.right.equalTo(-self.margin)
+ }
+
+ self.rateLabel.textAlignment = .center
+ self.rateLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
+ self.rateLabel.numberOfLines = 0
+ self.rateLabel.snp.makeConstraints { make in
+ make.left.equalTo(self.thumbnailImageView.snp.right).offset(5)
+ make.centerY.equalTo(self.thumbnailImageView.snp.centerY).offset(8)
make.right.equalTo(-self.margin)
}
}
diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift
index bfcc9d5e..bbb47314 100644
--- a/BeeSwift/GoalViewController.swift
+++ b/BeeSwift/GoalViewController.swift
@@ -507,29 +507,13 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
try await ServiceLocator.goalManager.refreshGoal(self.goal.objectID)
updateInterfaceToMatchGoal()
}
-
- private static var goalRateNumberFormatter: NumberFormatter {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.maximumFractionDigits = 3
- return formatter
- }
-
- private var formattedGoalRate: String? {
- guard let rate = goal.goalRate as NSNumber? else { return nil }
-
- return Self.goalRateNumberFormatter.string(from: rate)
- }
func updateInterfaceToMatchGoal() {
self.datapointTableController.hhmmformat = goal.hhmmFormat
self.datapointTableController.datapoints = goal.recentData.sorted(by: {$0.updatedAt < $1.updatedAt})
- self.goalRateLabel.isHidden = formattedGoalRate == nil
- self.goalRateLabel.text = {
- guard let formattedGoalRate else { return nil }
- return "\(formattedGoalRate) \(goal.goalUnits) / \(goal.rateUnits)"
- }()
+ self.goalRateLabel.isHidden = goal.currentRate == nil
+ self.goalRateLabel.text = goal.currentRate
self.refreshCountdown()
self.updateLastUpdatedLabel()