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