From 2993f4766fd4f9b25580f7567623d3ca2ca4d2a8 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:29:26 +0100 Subject: [PATCH 1/7] test of goalvc / add data initial stepper value --- BeeSwift.xcodeproj/project.pbxproj | 8 ++ BeeSwift/Gallery/GalleryViewController.swift | 3 +- BeeSwift/GoalViewController.swift | 21 ++--- BeeSwift/GoalViewModel.swift | 24 ++++++ BeeSwiftTests/GoalViewModelTests.swift | 86 ++++++++++++++++++++ 5 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 BeeSwift/GoalViewModel.swift create mode 100644 BeeSwiftTests/GoalViewModelTests.swift diff --git a/BeeSwift.xcodeproj/project.pbxproj b/BeeSwift.xcodeproj/project.pbxproj index f74933ee..5d4444d5 100644 --- a/BeeSwift.xcodeproj/project.pbxproj +++ b/BeeSwift.xcodeproj/project.pbxproj @@ -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 */; }; @@ -218,6 +220,8 @@ /* Begin PBXFileReference section */ 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 9BEB2D2A2CF3ED9A00D36ED1 /* GoalViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalViewModelTests.swift; sourceTree = ""; }; + 9BEB2D2C2CF3EF7E00D36ED1 /* GoalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalViewModel.swift; sourceTree = ""; }; A10D4E921B07948500A72D29 /* DatapointsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatapointsTableView.swift; sourceTree = ""; }; A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveHKMetricViewController.swift; sourceTree = ""; }; A11A87C51FEBFF7200A43E47 /* ChooseGoalSortViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseGoalSortViewController.swift; sourceTree = ""; }; @@ -514,6 +518,7 @@ E43BEA852A036D4300FC3A38 /* LogReaderTests.swift */, E417572C2A6446FE0029CDDA /* CurrentUserManagerTests.swift */, E4B6FEC52A776A2900690376 /* GoalTests.swift */, + 9BEB2D2A2CF3ED9A00D36ED1 /* GoalViewModelTests.swift */, ); path = BeeSwiftTests; sourceTree = ""; @@ -605,6 +610,7 @@ children = ( A1F9D1E9211B9B7600E2BC93 /* EditDatapointViewController.swift */, A1BD0D171AEB30A5001EDE8B /* GoalViewController.swift */, + 9BEB2D2C2CF3EF7E00D36ED1 /* GoalViewModel.swift */, A11BC2D81FFAD5BC00E56064 /* TimerViewController.swift */, ); name = GoalView; @@ -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 */, @@ -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 */, ); diff --git a/BeeSwift/Gallery/GalleryViewController.swift b/BeeSwift/Gallery/GalleryViewController.swift index be06c5d5..826a062d 100644 --- a/BeeSwift/Gallery/GalleryViewController.swift +++ b/BeeSwift/Gallery/GalleryViewController.swift @@ -489,7 +489,8 @@ class GalleryViewController: UIViewController, UICollectionViewDelegateFlowLayou } func openGoal(_ goal: Goal) { - let goalViewController = GoalViewController(goal: goal) + let viewModel = GoalViewModel(goal: goal) + let goalViewController = GoalViewController(viewModel: viewModel) self.navigationController?.pushViewController(goalViewController, animated: true) } diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift index a07f27a2..830c0592 100644 --- a/BeeSwift/GoalViewController.swift +++ b/BeeSwift/GoalViewController.swift @@ -22,7 +22,11 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl private let logger = Logger(subsystem: "com.beeminder.com", category: "GoalViewController") - let goal: Goal + var goal: Goal { + viewModel.goal + } + + private let viewModel: GoalViewModel fileprivate var goalImageView = GoalImageView(isThumbnail: false) fileprivate var datapointTableController = DatapointTableViewController() @@ -43,8 +47,8 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl // date corresponding to the datapoint to be created private var date: Date = Date() - init(goal: Goal) { - self.goal = goal + init(viewModel: GoalViewModel) { + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -212,7 +216,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) @@ -495,15 +499,6 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl 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 diff --git a/BeeSwift/GoalViewModel.swift b/BeeSwift/GoalViewModel.swift new file mode 100644 index 00000000..630b2827 --- /dev/null +++ b/BeeSwift/GoalViewModel.swift @@ -0,0 +1,24 @@ +// +// GoalViewModel.swift +// BeeSwift +// +// Created by krugerk on 2024-11-25. +// + +import Foundation +import Intents + +import BeeKit + +struct GoalViewModel { + let goal: Goal + + public func initialDateStepperValue(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)) + } +} diff --git a/BeeSwiftTests/GoalViewModelTests.swift b/BeeSwiftTests/GoalViewModelTests.swift new file mode 100644 index 00000000..61fbfc0d --- /dev/null +++ b/BeeSwiftTests/GoalViewModelTests.swift @@ -0,0 +1,86 @@ +// +// GoalViewModelTests.swift +// BeeSwiftTests +// +// Created by krugerk on 2024-11-25. +// + +import Testing + +@testable import BeeSwift +@testable import BeeKit + +struct GoalViewModelTests { + + @Test func initialStepperIsMinusOneWhenSubmissionDateIsAfterMidnightAndBeforeDeadline() async throws { + let goal = Self.makeGoalWithDeadline(3600 * 3) + let viewModel = GoalViewModel(goal: goal) + let submissionDate = Calendar.current.date(bySettingHour: 1, minute: 30, second: 0, of: Date())! + let actual = viewModel.initialDateStepperValue(date: submissionDate) + #expect(actual == -1) + } + + @Test func initialStepperIsZeroWhenSubmissionDateIsBeforeMidnightAndBeforeDeadline() async throws { + let goal = Self.makeGoalWithDeadline(0) + let viewModel = GoalViewModel(goal: goal) + let submissionDate = Calendar.current.date(bySettingHour: 20, minute: 30, second: 0, of: Date())! + let actual = viewModel.initialDateStepperValue(date: submissionDate) + #expect(actual == 0) + } + + @Test func initialStepperIsPlusOneWhenSubmissionDateIsAfterDeadline() async throws { + let goal = Self.makeGoalWithDeadline(3600 * -3) + let viewModel = GoalViewModel(goal: goal) + let submissionDate = Calendar.current.date(bySettingHour: 22, minute: 30, second: 0, of: Date())! + let actual = viewModel.initialDateStepperValue(date: submissionDate) + #expect(actual == 1) + } +} + + + +private extension GoalViewModelTests { + static func makeGoalWithDeadline(_ deadline: Int) -> Goal { + let context = BeeminderPersistentContainer.createMemoryBackedForTests().newBackgroundContext() + + let user = User(context: context, + username: "user123", + deadbeat: false, + timezone: "", + defaultAlertStart: 0, + defaultDeadline: 0, + defaultLeadTime: 0) + + let goal = Goal(context: context, + owner: user, + id: "goalid", + slug: "goalname", + alertStart: 0, + autodata: nil, + deadline: deadline, + graphUrl: "", + healthKitMetric: "", + hhmmFormat: false, + initDay: 0, + lastTouch: "", + limSum: "", + leadTime: 0, + pledge: 801, + queued: false, + safeBuf: 0, + safeSum: "", + thumbUrl: "", + title: "goaldescription", + todayta: false, + urgencyKey: "urgencyKey", + useDefaults: false, + won: false, + yAxis: "units") + + context.perform { + try! context.save() + } + + return goal + } +} From 9f9f3329b5c71d47ce69bbd2281fd47def5cca52 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:12:22 +0100 Subject: [PATCH 2/7] rather than feeding goalvc a goalvm --- BeeSwift/Gallery/GalleryViewController.swift | 3 +-- BeeSwift/GoalViewController.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/BeeSwift/Gallery/GalleryViewController.swift b/BeeSwift/Gallery/GalleryViewController.swift index 826a062d..be06c5d5 100644 --- a/BeeSwift/Gallery/GalleryViewController.swift +++ b/BeeSwift/Gallery/GalleryViewController.swift @@ -489,8 +489,7 @@ class GalleryViewController: UIViewController, UICollectionViewDelegateFlowLayou } func openGoal(_ goal: Goal) { - let viewModel = GoalViewModel(goal: goal) - let goalViewController = GoalViewController(viewModel: viewModel) + let goalViewController = GoalViewController(goal: goal) self.navigationController?.pushViewController(goalViewController, animated: true) } diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift index 830c0592..f1f06524 100644 --- a/BeeSwift/GoalViewController.swift +++ b/BeeSwift/GoalViewController.swift @@ -47,8 +47,8 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl // date corresponding to the datapoint to be created private var date: Date = Date() - init(viewModel: GoalViewModel) { - self.viewModel = viewModel + init(goal: Goal) { + self.viewModel = .init(goal: goal) super.init(nibName: nil, bundle: nil) } From 00f8d11aeab4b40b14fea2a081c4635f31ce3e0f Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:48:51 +0100 Subject: [PATCH 3/7] migrating to goalVM --- BeeSwift/GoalViewController.swift | 55 ++++++++++------------ BeeSwift/GoalViewModel.swift | 63 ++++++++++++++++++++++++++ BeeSwiftTests/GoalViewModelTests.swift | 26 +++++------ 3 files changed, 97 insertions(+), 47 deletions(-) diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift index f1f06524..4f318764 100644 --- a/BeeSwift/GoalViewController.swift +++ b/BeeSwift/GoalViewController.swift @@ -21,10 +21,6 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl let buttonHeight = 42 private let logger = Logger(subsystem: "com.beeminder.com", category: "GoalViewController") - - var goal: Goal { - viewModel.goal - } private let viewModel: GoalViewModel @@ -58,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 @@ -128,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) @@ -140,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 @@ -261,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) @@ -279,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))) } @@ -308,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 @@ -332,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 { @@ -352,8 +344,8 @@ 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() { @@ -361,10 +353,10 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl } 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) @@ -386,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() } @@ -459,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() @@ -485,13 +476,13 @@ 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() } diff --git a/BeeSwift/GoalViewModel.swift b/BeeSwift/GoalViewModel.swift index 630b2827..939911fc 100644 --- a/BeeSwift/GoalViewModel.swift +++ b/BeeSwift/GoalViewModel.swift @@ -8,6 +8,7 @@ import Foundation import Intents +import CoreData import BeeKit struct GoalViewModel { @@ -21,4 +22,66 @@ struct GoalViewModel { return Double(daystampAssumingMidnightDeadline.distance(to: daystampAccountingForTheGoalsDeadline)) } + + 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 + } } diff --git a/BeeSwiftTests/GoalViewModelTests.swift b/BeeSwiftTests/GoalViewModelTests.swift index 61fbfc0d..3960b8f1 100644 --- a/BeeSwiftTests/GoalViewModelTests.swift +++ b/BeeSwiftTests/GoalViewModelTests.swift @@ -1,44 +1,40 @@ -// -// GoalViewModelTests.swift -// BeeSwiftTests -// -// Created by krugerk on 2024-11-25. -// - import Testing @testable import BeeSwift @testable import BeeKit struct GoalViewModelTests { + private enum DayStep: Double { + case yesterday = -1 + case today = 0 + case tomorrow = 1 + } - @Test func initialStepperIsMinusOneWhenSubmissionDateIsAfterMidnightAndBeforeDeadline() async throws { + @Test func initialStepperIsYesterdayWhenSubmissionDateIsAfterMidnightAndBeforeDeadline() async throws { let goal = Self.makeGoalWithDeadline(3600 * 3) let viewModel = GoalViewModel(goal: goal) let submissionDate = Calendar.current.date(bySettingHour: 1, minute: 30, second: 0, of: Date())! let actual = viewModel.initialDateStepperValue(date: submissionDate) - #expect(actual == -1) + #expect(actual == DayStep.yesterday.rawValue) } - @Test func initialStepperIsZeroWhenSubmissionDateIsBeforeMidnightAndBeforeDeadline() async throws { + @Test func initialStepperIsTodayWhenSubmissionDateIsBeforeMidnightAndBeforeDeadline() async throws { let goal = Self.makeGoalWithDeadline(0) let viewModel = GoalViewModel(goal: goal) let submissionDate = Calendar.current.date(bySettingHour: 20, minute: 30, second: 0, of: Date())! let actual = viewModel.initialDateStepperValue(date: submissionDate) - #expect(actual == 0) + #expect(actual == DayStep.today.rawValue) } - @Test func initialStepperIsPlusOneWhenSubmissionDateIsAfterDeadline() async throws { + @Test func initialStepperIsTomorrowWhenSubmissionDateIsAfterDeadline() async throws { let goal = Self.makeGoalWithDeadline(3600 * -3) let viewModel = GoalViewModel(goal: goal) let submissionDate = Calendar.current.date(bySettingHour: 22, minute: 30, second: 0, of: Date())! let actual = viewModel.initialDateStepperValue(date: submissionDate) - #expect(actual == 1) + #expect(actual == DayStep.tomorrow.rawValue) } } - - private extension GoalViewModelTests { static func makeGoalWithDeadline(_ deadline: Int) -> Goal { let context = BeeminderPersistentContainer.createMemoryBackedForTests().newBackgroundContext() From afa831d2f880a351d3bd592461178cf54c3a9641 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:52:43 +0100 Subject: [PATCH 4/7] some context on why PlusOne is the correct value --- BeeSwiftTests/GoalViewModelTests.swift | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/BeeSwiftTests/GoalViewModelTests.swift b/BeeSwiftTests/GoalViewModelTests.swift index 3960b8f1..0077df56 100644 --- a/BeeSwiftTests/GoalViewModelTests.swift +++ b/BeeSwiftTests/GoalViewModelTests.swift @@ -5,33 +5,33 @@ import Testing struct GoalViewModelTests { private enum DayStep: Double { - case yesterday = -1 - case today = 0 - case tomorrow = 1 + case previousDay = -1 + case sameDay = 0 + case nextDay = 1 } @Test func initialStepperIsYesterdayWhenSubmissionDateIsAfterMidnightAndBeforeDeadline() async throws { - let goal = Self.makeGoalWithDeadline(3600 * 3) - let viewModel = GoalViewModel(goal: goal) - let submissionDate = Calendar.current.date(bySettingHour: 1, minute: 30, second: 0, of: Date())! - let actual = viewModel.initialDateStepperValue(date: submissionDate) - #expect(actual == DayStep.yesterday.rawValue) + let goalWithAfterMidnightDeadline = Self.makeGoalWithDeadline(3600 * 3) + let viewModel = GoalViewModel(goal: goalWithAfterMidnightDeadline) + let submissionDateBeforeGoalsDeadline = Calendar.current.date(bySettingHour: 1, minute: 30, second: 0, of: Date())! + let actual = viewModel.initialDateStepperValue(date: submissionDateBeforeGoalsDeadline) + #expect(actual == DayStep.previousDay.rawValue) } @Test func initialStepperIsTodayWhenSubmissionDateIsBeforeMidnightAndBeforeDeadline() async throws { - let goal = Self.makeGoalWithDeadline(0) - let viewModel = GoalViewModel(goal: goal) - let submissionDate = Calendar.current.date(bySettingHour: 20, minute: 30, second: 0, of: Date())! - let actual = viewModel.initialDateStepperValue(date: submissionDate) - #expect(actual == DayStep.today.rawValue) + let goalWithMidnightDeadline = Self.makeGoalWithDeadline(0) + let viewModel = GoalViewModel(goal: goalWithMidnightDeadline) + let submissionDateBeforeMidnight = Calendar.current.date(bySettingHour: 20, minute: 30, second: 0, of: Date())! + let actual = viewModel.initialDateStepperValue(date: submissionDateBeforeMidnight) + #expect(actual == DayStep.sameDay.rawValue) } @Test func initialStepperIsTomorrowWhenSubmissionDateIsAfterDeadline() async throws { - let goal = Self.makeGoalWithDeadline(3600 * -3) - let viewModel = GoalViewModel(goal: goal) - let submissionDate = Calendar.current.date(bySettingHour: 22, minute: 30, second: 0, of: Date())! - let actual = viewModel.initialDateStepperValue(date: submissionDate) - #expect(actual == DayStep.tomorrow.rawValue) + let goalWithBeforeMidnightDeadline = Self.makeGoalWithDeadline(3600 * -3) + let viewModel = GoalViewModel(goal: goalWithBeforeMidnightDeadline) + let submissionDateBetweenDeadlineAndMidnight = Calendar.current.date(bySettingHour: 22, minute: 30, second: 0, of: Date())! + let actual = viewModel.initialDateStepperValue(date: submissionDateBetweenDeadlineAndMidnight) + #expect(actual == DayStep.nextDay.rawValue) } } From 2daaa2f6b27716e59c26965165b1ac6050ac731a Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:00:35 +0100 Subject: [PATCH 5/7] clean code --- BeeSwift/GoalViewModel.swift | 26 +++++++++----------------- BeeSwiftTests/GoalViewModelTests.swift | 6 +++--- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/BeeSwift/GoalViewModel.swift b/BeeSwift/GoalViewModel.swift index 939911fc..d3d034de 100644 --- a/BeeSwift/GoalViewModel.swift +++ b/BeeSwift/GoalViewModel.swift @@ -1,12 +1,4 @@ -// -// GoalViewModel.swift -// BeeSwift -// -// Created by krugerk on 2024-11-25. -// - import Foundation -import Intents import CoreData import BeeKit @@ -14,15 +6,6 @@ import BeeKit struct GoalViewModel { let goal: Goal - public func initialDateStepperValue(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)) - } - var title: String { goal.slug } @@ -84,4 +67,13 @@ struct GoalViewModel { 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)) + } } diff --git a/BeeSwiftTests/GoalViewModelTests.swift b/BeeSwiftTests/GoalViewModelTests.swift index 0077df56..1f1c4a3c 100644 --- a/BeeSwiftTests/GoalViewModelTests.swift +++ b/BeeSwiftTests/GoalViewModelTests.swift @@ -14,7 +14,7 @@ struct GoalViewModelTests { let goalWithAfterMidnightDeadline = Self.makeGoalWithDeadline(3600 * 3) let viewModel = GoalViewModel(goal: goalWithAfterMidnightDeadline) let submissionDateBeforeGoalsDeadline = Calendar.current.date(bySettingHour: 1, minute: 30, second: 0, of: Date())! - let actual = viewModel.initialDateStepperValue(date: submissionDateBeforeGoalsDeadline) + let actual = viewModel.initialDateStepperValue(submissionDate: submissionDateBeforeGoalsDeadline) #expect(actual == DayStep.previousDay.rawValue) } @@ -22,7 +22,7 @@ struct GoalViewModelTests { let goalWithMidnightDeadline = Self.makeGoalWithDeadline(0) let viewModel = GoalViewModel(goal: goalWithMidnightDeadline) let submissionDateBeforeMidnight = Calendar.current.date(bySettingHour: 20, minute: 30, second: 0, of: Date())! - let actual = viewModel.initialDateStepperValue(date: submissionDateBeforeMidnight) + let actual = viewModel.initialDateStepperValue(submissionDate: submissionDateBeforeMidnight) #expect(actual == DayStep.sameDay.rawValue) } @@ -30,7 +30,7 @@ struct GoalViewModelTests { let goalWithBeforeMidnightDeadline = Self.makeGoalWithDeadline(3600 * -3) let viewModel = GoalViewModel(goal: goalWithBeforeMidnightDeadline) let submissionDateBetweenDeadlineAndMidnight = Calendar.current.date(bySettingHour: 22, minute: 30, second: 0, of: Date())! - let actual = viewModel.initialDateStepperValue(date: submissionDateBetweenDeadlineAndMidnight) + let actual = viewModel.initialDateStepperValue(submissionDate: submissionDateBetweenDeadlineAndMidnight) #expect(actual == DayStep.nextDay.rawValue) } } From 96bd472b3b7ce9c33a7b810c2e9608db87942602 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:39:10 +0100 Subject: [PATCH 6/7] fastlane is not picking up swift-testing tests back to xctestcase for now --- BeeSwiftTests/GoalViewModelTests.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/BeeSwiftTests/GoalViewModelTests.swift b/BeeSwiftTests/GoalViewModelTests.swift index 1f1c4a3c..2856f8eb 100644 --- a/BeeSwiftTests/GoalViewModelTests.swift +++ b/BeeSwiftTests/GoalViewModelTests.swift @@ -1,37 +1,40 @@ -import Testing +import XCTest @testable import BeeSwift @testable import BeeKit -struct GoalViewModelTests { +final class GoalViewModelTests: XCTestCase { private enum DayStep: Double { case previousDay = -1 case sameDay = 0 case nextDay = 1 } - @Test func initialStepperIsYesterdayWhenSubmissionDateIsAfterMidnightAndBeforeDeadline() async throws { + func testInitialStepperIsYesterdayWhenSubmissionDateIsAfterMidnightAndBeforeDeadline() async throws { let goalWithAfterMidnightDeadline = Self.makeGoalWithDeadline(3600 * 3) let viewModel = GoalViewModel(goal: goalWithAfterMidnightDeadline) let submissionDateBeforeGoalsDeadline = Calendar.current.date(bySettingHour: 1, minute: 30, second: 0, of: Date())! let actual = viewModel.initialDateStepperValue(submissionDate: submissionDateBeforeGoalsDeadline) - #expect(actual == DayStep.previousDay.rawValue) + XCTAssertEqual(actual, + DayStep.previousDay.rawValue) } - @Test func initialStepperIsTodayWhenSubmissionDateIsBeforeMidnightAndBeforeDeadline() async throws { + func testInitialStepperIsTodayWhenSubmissionDateIsBeforeMidnightAndBeforeDeadline() async throws { let goalWithMidnightDeadline = Self.makeGoalWithDeadline(0) let viewModel = GoalViewModel(goal: goalWithMidnightDeadline) let submissionDateBeforeMidnight = Calendar.current.date(bySettingHour: 20, minute: 30, second: 0, of: Date())! let actual = viewModel.initialDateStepperValue(submissionDate: submissionDateBeforeMidnight) - #expect(actual == DayStep.sameDay.rawValue) + XCTAssertEqual(actual, + DayStep.sameDay.rawValue) } - - @Test func initialStepperIsTomorrowWhenSubmissionDateIsAfterDeadline() async throws { + + func testInitialStepperIsTomorrowWhenSubmissionDateIsAfterDeadline() async throws { let goalWithBeforeMidnightDeadline = Self.makeGoalWithDeadline(3600 * -3) let viewModel = GoalViewModel(goal: goalWithBeforeMidnightDeadline) let submissionDateBetweenDeadlineAndMidnight = Calendar.current.date(bySettingHour: 22, minute: 30, second: 0, of: Date())! let actual = viewModel.initialDateStepperValue(submissionDate: submissionDateBetweenDeadlineAndMidnight) - #expect(actual == DayStep.nextDay.rawValue) + XCTAssertEqual(actual, + DayStep.nextDay.rawValue) } } From 9b56b357e1ddd51206ae6804e06452f6c53418a5 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:19:22 +0100 Subject: [PATCH 7/7] gather code coverage - enabled --- BeeSwift.xcodeproj/xcshareddata/xcschemes/BeeSwift.xcscheme | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BeeSwift.xcodeproj/xcshareddata/xcschemes/BeeSwift.xcscheme b/BeeSwift.xcodeproj/xcshareddata/xcschemes/BeeSwift.xcscheme index 9423f105..65fc2409 100644 --- a/BeeSwift.xcodeproj/xcshareddata/xcschemes/BeeSwift.xcscheme +++ b/BeeSwift.xcodeproj/xcshareddata/xcschemes/BeeSwift.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES">