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