Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test of goalvc / add data initial stepper value #529

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions BeeSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/* Begin PBXBuildFile section */
9B8CA57D24B120CA009C86C2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */; };
9BEB2D2B2CF3ED9A00D36ED1 /* GoalViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEB2D2A2CF3ED9A00D36ED1 /* GoalViewModelTests.swift */; };
9BEB2D2D2CF3EF7E00D36ED1 /* GoalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEB2D2C2CF3EF7E00D36ED1 /* GoalViewModel.swift */; };
A10D4E931B07948500A72D29 /* DatapointsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10D4E921B07948500A72D29 /* DatapointsTableView.swift */; };
A10DC2DF207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */; };
A11A87C61FEBFF7200A43E47 /* ChooseGoalSortViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A87C51FEBFF7200A43E47 /* ChooseGoalSortViewController.swift */; };
Expand Down Expand Up @@ -218,6 +220,8 @@

/* Begin PBXFileReference section */
9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
9BEB2D2A2CF3ED9A00D36ED1 /* GoalViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalViewModelTests.swift; sourceTree = "<group>"; };
9BEB2D2C2CF3EF7E00D36ED1 /* GoalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalViewModel.swift; sourceTree = "<group>"; };
A10D4E921B07948500A72D29 /* DatapointsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatapointsTableView.swift; sourceTree = "<group>"; };
A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveHKMetricViewController.swift; sourceTree = "<group>"; };
A11A87C51FEBFF7200A43E47 /* ChooseGoalSortViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseGoalSortViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -514,6 +518,7 @@
E43BEA852A036D4300FC3A38 /* LogReaderTests.swift */,
E417572C2A6446FE0029CDDA /* CurrentUserManagerTests.swift */,
E4B6FEC52A776A2900690376 /* GoalTests.swift */,
9BEB2D2A2CF3ED9A00D36ED1 /* GoalViewModelTests.swift */,
);
path = BeeSwiftTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -605,6 +610,7 @@
children = (
A1F9D1E9211B9B7600E2BC93 /* EditDatapointViewController.swift */,
A1BD0D171AEB30A5001EDE8B /* GoalViewController.swift */,
9BEB2D2C2CF3EF7E00D36ED1 /* GoalViewModel.swift */,
A11BC2D81FFAD5BC00E56064 /* TimerViewController.swift */,
);
name = GoalView;
Expand Down Expand Up @@ -1013,6 +1019,7 @@
A1453B3F1AEDFCC8006F48DA /* SignInViewController.swift in Sources */,
A1E618E41E7934C700D8ED93 /* HealthKitConfigTableViewCell.swift in Sources */,
E4B083392932F90400A71564 /* ConfigureHKMetricViewController.swift in Sources */,
9BEB2D2D2CF3EF7E00D36ED1 /* GoalViewModel.swift in Sources */,
E43BEA842A036A9C00FC3A38 /* LogReader.swift in Sources */,
A196CB1F1AE4142F00B90A3E /* GalleryViewController.swift in Sources */,
A1BE73AA1E8B45BF00DEC4DB /* ChooseHKMetricViewController.swift in Sources */,
Expand Down Expand Up @@ -1054,6 +1061,7 @@
E4B0A32E28C194C800055EA7 /* AddDataIntents.intentdefinition in Sources */,
E48E2714296B75E4008013C0 /* TotalSleepMinutesTests.swift in Sources */,
A196CB331AE4142F00B90A3E /* BeeSwiftTests.swift in Sources */,
9BEB2D2B2CF3ED9A00D36ED1 /* GoalViewModelTests.swift in Sources */,
E4B6FEC62A776A2900690376 /* GoalTests.swift in Sources */,
E43BEA862A036D4300FC3A38 /* LogReaderTests.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
Expand Down
68 changes: 27 additions & 41 deletions BeeSwift/GoalViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
let buttonHeight = 42

private let logger = Logger(subsystem: "com.beeminder.com", category: "GoalViewController")

let goal: Goal
private let viewModel: GoalViewModel

fileprivate var goalImageView = GoalImageView(isThumbnail: false)
fileprivate var datapointTableController = DatapointTableViewController()
Expand All @@ -44,7 +44,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
private var date: Date = Date()

init(goal: Goal) {
self.goal = goal
self.viewModel = .init(goal: goal)
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -54,7 +54,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl

override func viewDidLoad() {
self.view.backgroundColor = UIColor.systemBackground
self.title = self.goal.slug
self.title = viewModel.title

// have to set these before the datapoints since setting the most recent datapoint updates the text field,
// which in turn updates the stepper
Expand Down Expand Up @@ -124,7 +124,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
make.left.equalTo(self.goalImageScrollView)
make.right.equalTo(self.goalImageScrollView)
}
self.goalImageView.goal = self.goal
self.goalImageView.goal = self.viewModel.goal

self.addChild(self.datapointTableController)
self.scrollView.addSubview(self.datapointTableController.view)
Expand All @@ -136,7 +136,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

let dataEntryView = UIView()
dataEntryView.isHidden = self.goal.hideDataEntry
dataEntryView.isHidden = viewModel.isDataEntryHidden

self.scrollView.addSubview(dataEntryView)
dataEntryView.snp.makeConstraints { (make) -> Void in
Expand Down Expand Up @@ -212,7 +212,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
self.dateStepper.tintColor = UIColor.Beeminder.gray
dataEntryView.addSubview(self.dateStepper)
self.dateStepper.addTarget(self, action: #selector(GoalViewController.dateStepperValueChanged), for: .valueChanged)
self.dateStepper.value = Self.makeInitialDateStepperValue(for: goal)
self.dateStepper.value = viewModel.initialDateStepperValue()

self.dateStepper.snp.makeConstraints { (make) -> Void in
make.top.equalTo(self.dateTextField.snp.bottom).offset(elementSpacing)
Expand Down Expand Up @@ -257,15 +257,11 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
make.bottom.equalTo(self.submitButton)
}

if self.goal.isDataProvidedAutomatically {
if viewModel.showPullToRefreshHint {
let pullToRefreshView = PullToRefreshView()
scrollView.addSubview(pullToRefreshView)

if self.goal.isLinkedToHealthKit {
pullToRefreshView.message = "Pull down to synchronize with Apple Health"
} else {
pullToRefreshView.message = "Pull down to update"
}
pullToRefreshView.message = viewModel.pullToRefreshHint

pullToRefreshView.snp.makeConstraints { (make) in
make.top.equalTo(self.datapointTableController.view.snp.bottom).offset(elementSpacing)
Expand All @@ -275,7 +271,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

self.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(self.actionButtonPressed))]
if !self.goal.hideDataEntry {
if viewModel.showTimerButton {
self.navigationItem.rightBarButtonItems?.append(UIBarButtonItem(image: UIImage(systemName: "stopwatch"), style: .plain, target: self, action: #selector(self.timerButtonPressed)))
}

Expand Down Expand Up @@ -304,21 +300,21 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
do {
try await self.updateGoalAndInterface()
} catch {
logger.error("Error refreshing details for goal \(self.goal.slug): \(error)")
logger.error("Error refreshing details for goal \(self.viewModel.goalName): \(error)")
}
}
}

@objc func timerButtonPressed() {
let controller = TimerViewController(goal: self.goal)
let controller = TimerViewController(goal: viewModel.goal)
controller.modalPresentationStyle = .fullScreen
self.present(controller, animated: true, completion: nil)
}

@objc func actionButtonPressed() {
let username = goal.owner.username
let username = viewModel.username
guard let accessToken = ServiceLocator.currentUserManager.accessToken,
let viewGoalUrl = URL(string: "\(ServiceLocator.requestManager.baseURLString)/api/v1/users/\(username).json?access_token=\(accessToken)&redirect_to_url=\(ServiceLocator.requestManager.baseURLString)/\(username)/\(self.goal.slug)") else { return }
let viewGoalUrl = URL(string: "\(ServiceLocator.requestManager.baseURLString)/api/v1/users/\(username).json?access_token=\(accessToken)&redirect_to_url=\(ServiceLocator.requestManager.baseURLString)/\(username)/\(viewModel.goalName)") else { return }

let safariVC = SFSafariViewController(url: viewGoalUrl)
safariVC.delegate = self
Expand All @@ -328,12 +324,12 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
@objc func refreshButtonPressed() {
Task { @MainActor in
do {
if self.goal.isLinkedToHealthKit {
try await ServiceLocator.healthStoreManager.updateWithRecentData(goalID: self.goal.objectID, days: 7)
} else if goal.isDataProvidedAutomatically {
if viewModel.isLinkedWithHealthKit {
try await ServiceLocator.healthStoreManager.updateWithRecentData(goalID: self.viewModel.goalObjectId, days: 7)
} else if !viewModel.usesManualDataEntry {
// Don't force a refresh for manual goals. While doing so is harmless, it queues the goal which means we show a
// lemniscate for a few seconds, making the refresh slower.
try await ServiceLocator.goalManager.forceAutodataRefresh(self.goal)
try await ServiceLocator.goalManager.forceAutodataRefresh(self.viewModel.goal)
}
try await self.updateGoalAndInterface()
} catch {
Expand All @@ -348,19 +344,19 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

@objc func refreshCountdown() {
self.countdownLabel.textColor = self.goal.countdownColor
self.countdownLabel.text = self.goal.capitalSafesum()
self.countdownLabel.textColor = viewModel.countdownLabelTextColor
self.countdownLabel.text = viewModel.countdownLabelText
}

@objc func goalImageTapped() {
self.goalImageScrollView.setZoomScale(self.goalImageScrollView.zoomScale == 1.0 ? 2.0 : 1.0, animated: true)
}

func datapointTableViewController(_ datapointTableViewController: DatapointTableViewController, didSelectDatapoint datapoint: BeeDataPoint) {
guard !self.goal.hideDataEntry else { return }
guard !viewModel.isDataEntryHidden else { return }
guard let existingDatapoint = datapoint as? DataPoint else { return }

let editDatapointViewController = EditDatapointViewController(goal: goal, datapoint: existingDatapoint)
let editDatapointViewController = EditDatapointViewController(goal: viewModel.goal, datapoint: existingDatapoint)
let navigationController = UINavigationController(rootViewController: editDatapointViewController)
navigationController.modalPresentationStyle = .formSheet
self.present(navigationController, animated: true, completion: nil)
Expand All @@ -382,8 +378,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

func setValueTextField() {
let suggestedNextValue = goal.suggestedNextValue ?? 1
valueTextField.text = "\(String(describing: suggestedNextValue))"
valueTextField.text = "\(viewModel.suggestedNextValue)"
valueTextFieldValueChanged()
}

Expand Down Expand Up @@ -455,7 +450,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
self.scrollView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 0, height: 0), animated: true)

do {
let _ = try await ServiceLocator.requestManager.addDatapoint(urtext: self.urtext, slug: self.goal.slug)
let _ = try await ServiceLocator.requestManager.addDatapoint(urtext: self.urtext, slug: viewModel.goalName)
self.commentTextField.text = ""

try await updateGoalAndInterface()
Expand All @@ -481,29 +476,20 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

func updateGoalAndInterface() async throws {
try await ServiceLocator.goalManager.refreshGoal(self.goal.objectID)
try await ServiceLocator.goalManager.refreshGoal(self.viewModel.goalObjectId)
updateInterfaceToMatchGoal()
}

func updateInterfaceToMatchGoal() {
self.datapointTableController.hhmmformat = goal.hhmmFormat
self.datapointTableController.datapoints = goal.recentData.sorted(by: {$0.updatedAt < $1.updatedAt})
self.datapointTableController.hhmmformat = viewModel.isHhmmFormat
self.datapointTableController.datapoints = viewModel.recentDatapoints

self.refreshCountdown()
}

func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.goalImageView
}

private static func makeInitialDateStepperValue(date: Date = Date(), for goal: Goal) -> Double {
let daystampAccountingForTheGoalsDeadline = Daystamp(fromDate: date,
deadline: goal.deadline)
let daystampAssumingMidnightDeadline = Daystamp(fromDate: date,
deadline: 0)

return Double(daystampAssumingMidnightDeadline.distance(to: daystampAccountingForTheGoalsDeadline))
}

// MARK: - SFSafariViewControllerDelegate

Expand Down
79 changes: 79 additions & 0 deletions BeeSwift/GoalViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation

import CoreData
import BeeKit

struct GoalViewModel {
let goal: Goal

var title: String {
goal.slug
}

var isDataEntryHidden: Bool {
goal.hideDataEntry
}

var showPullToRefreshHint: Bool {
goal.isDataProvidedAutomatically
}

var pullToRefreshHint: String {
self.goal.isLinkedToHealthKit
? "Pull down to synchronize with Apple Health"
: "Pull down to update"
}

var goalName: String {
goal.slug
}

var username: String {
goal.owner.username
}

var countdownLabelTextColor: UIColor? {
goal.countdownColor
}

var countdownLabelText: String? {
goal.capitalSafesum()
}

var suggestedNextValue: NSNumber {
goal.suggestedNextValue ?? 1
}

var isHhmmFormat: Bool {
goal.hhmmFormat
}

var recentDatapoints: [DataPoint] {
goal.recentData.sorted(using: SortDescriptor(\.updatedAt))
}

var isLinkedWithHealthKit: Bool {
goal.isLinkedToHealthKit
}

var usesManualDataEntry: Bool {
!goal.isDataProvidedAutomatically
}

var goalObjectId: NSManagedObjectID {
goal.objectID
}

var showTimerButton: Bool {
!goal.hideDataEntry
}

func initialDateStepperValue(submissionDate date: Date = Date()) -> Double {
let daystampAccountingForTheGoalsDeadline = Daystamp(fromDate: date,
deadline: goal.deadline)
let daystampAssumingMidnightDeadline = Daystamp(fromDate: date,
deadline: 0)

return Double(daystampAssumingMidnightDeadline.distance(to: daystampAccountingForTheGoalsDeadline))
}
}
Loading
Loading