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/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
index cc994d00..aa5753d6 100644
--- a/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
+++ b/BeeKit/Model/BeeminderModel.xcdatamodeld/BeeminderModel.xcdatamodel/contents
@@ -14,6 +14,10 @@
+
+
+
+
@@ -25,6 +29,7 @@
+
diff --git a/BeeKit/Model/Goal.swift b/BeeKit/Model/Goal.swift
index 524b6edd..a6c347b0 100644
--- a/BeeKit/Model/Goal.swift
+++ b/BeeKit/Model/Goal.swift
@@ -51,7 +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.
+ @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:)
@@ -171,7 +184,23 @@ 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
+
+ // 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
let newRecentData = Set(json["recent_data"].arrayValue.map {
@@ -184,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/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 c5fd2d0a..bbb47314 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,9 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
self.datapointTableController.hhmmformat = goal.hhmmFormat
self.datapointTableController.datapoints = goal.recentData.sorted(by: {$0.updatedAt < $1.updatedAt})
+ self.goalRateLabel.isHidden = goal.currentRate == nil
+ self.goalRateLabel.text = goal.currentRate
+
self.refreshCountdown()
self.updateLastUpdatedLabel()
}