Skip to content

Commit

Permalink
Use Dependency Injection Everywhere (#582)
Browse files Browse the repository at this point in the history
## Summary
Remove almost all references to ServiceLocator. Instead use a dependency
injection pattern to pass required objects in via constructor
parameters.

## Validation
Launch the app in simulator. Verify I can navigate to the impacted
views:
* [x] Edit Datapoint
* [x] Gallery
* [x] Goal
* [x] Assign HK metric to goal
* [x] Configure notifications 
* [x] Edit default notifications
* [x] Remove HK metric
* [x] Settings
* [x] Timer
  • Loading branch information
theospears authored Jan 17, 2025
1 parent d181dba commit 205c8d4
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 75 deletions.
14 changes: 9 additions & 5 deletions BeeSwift/EditDatapointViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ class EditDatapointViewController: UIViewController, UITextFieldDelegate {
fileprivate var datePicker = InlineDatePicker()
fileprivate var valueField = UITextField()
fileprivate var commentField = UITextField()
private let requestManager: RequestManager
private let goalManager: GoalManager

init(goal: Goal, datapoint: DataPoint) {
init(goal: Goal, datapoint: DataPoint, requestManager: RequestManager, goalManager: GoalManager) {
self.goal = goal
self.datapoint = datapoint
self.requestManager = requestManager
self.goalManager = goalManager
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -213,8 +217,8 @@ class EditDatapointViewController: UIViewController, UITextFieldDelegate {
let params = [
"urtext": self.urtext()
]
let _ = try await ServiceLocator.requestManager.put(url: "api/v1/users/{username}/goals/\(self.goal.slug)/datapoints/\(self.datapoint.id).json", parameters: params)
try await ServiceLocator.goalManager.refreshGoal(self.goal.objectID)
let _ = try await self.requestManager.put(url: "api/v1/users/{username}/goals/\(self.goal.slug)/datapoints/\(self.datapoint.id).json", parameters: params)
try await self.goalManager.refreshGoal(self.goal.objectID)

hud.mode = .customView
hud.customView = UIImageView(image: UIImage(systemName: "checkmark"))
Expand All @@ -235,8 +239,8 @@ class EditDatapointViewController: UIViewController, UITextFieldDelegate {
hud.mode = .indeterminate

do {
let _ = try await ServiceLocator.requestManager.delete(url: "api/v1/users/{username}/goals/\(self.goal.slug)/datapoints/\(self.datapoint.id).json")
try await ServiceLocator.goalManager.refreshGoal(self.goal.objectID)
let _ = try await self.requestManager.delete(url: "api/v1/users/{username}/goals/\(self.goal.slug)/datapoints/\(self.datapoint.id).json")
try await self.goalManager.refreshGoal(self.goal.objectID)

hud.mode = .customView
hud.customView = UIImageView(image: UIImage(systemName: "checkmark"))
Expand Down
23 changes: 18 additions & 5 deletions BeeSwift/Gallery/GalleryViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class GalleryViewController: UIViewController {
private let versionManager: VersionManager
private let goalManager: GoalManager
private let healthStoreManager: HealthStoreManager
private let requestManager: RequestManager

private lazy var stackView: UIStackView = {
let stackView = UIStackView()
Expand Down Expand Up @@ -122,12 +123,14 @@ class GalleryViewController: UIViewController {
viewContext: NSManagedObjectContext,
versionManager: VersionManager,
goalManager: GoalManager,
healthStoreManager: HealthStoreManager) {
healthStoreManager: HealthStoreManager,
requestManager: RequestManager) {
self.currentUserManager = currentUserManager
self.viewContext = viewContext
self.versionManager = versionManager
self.goalManager = goalManager
self.healthStoreManager = healthStoreManager
self.requestManager = requestManager

let fetchRequest = Goal.fetchRequest() as! NSFetchRequest<Goal>
fetchRequest.sortDescriptors = Self.preferredSort
Expand Down Expand Up @@ -266,14 +269,18 @@ class GalleryViewController: UIViewController {

override func viewDidAppear(_ animated: Bool) {
if !currentUserManager.signedIn(context: viewContext) {
let signInVC = SignInViewController()
let signInVC = SignInViewController(currentUserManager: currentUserManager)
signInVC.modalPresentationStyle = .fullScreen
self.present(signInVC, animated: true, completion: nil)
}
}

@objc func settingsButtonPressed() {
self.navigationController?.pushViewController(SettingsViewController(), animated: true)
self.navigationController?.pushViewController(SettingsViewController(
currentUserManager: currentUserManager,
viewContext: viewContext,
goalManager: goalManager,
requestManager: requestManager), animated: true)
}

@objc func searchButtonPressed() {
Expand Down Expand Up @@ -305,7 +312,7 @@ class GalleryViewController: UIViewController {
if self.presentedViewController != nil {
if type(of: self.presentedViewController!) == SignInViewController.self { return }
}
let signInVC = SignInViewController()
let signInVC = SignInViewController(currentUserManager: currentUserManager)
signInVC.modalPresentationStyle = .fullScreen
self.present(signInVC, animated: true, completion: nil)
}
Expand Down Expand Up @@ -422,7 +429,13 @@ class GalleryViewController: UIViewController {
}

func openGoal(_ goal: Goal) {
let goalViewController = GoalViewController(goal: goal)
let goalViewController = GoalViewController(
goal: goal,
healthStoreManager: healthStoreManager,
goalManager: goalManager,
requestManager: requestManager,
currentUserManager: currentUserManager,
viewContext: viewContext)
self.navigationController?.pushViewController(goalViewController, animated: true)
}

Expand Down
42 changes: 30 additions & 12 deletions BeeSwift/GoalViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
//

import Foundation
import CoreData
import OSLog

import SwiftyJSON
import MBProgressHUD
import AlamofireImage
import SafariServices
import Intents

import BeeKit
import OSLog

class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTableViewControllerDelegate, UITextFieldDelegate, SFSafariViewControllerDelegate {
let elementSpacing = 10
Expand All @@ -23,6 +26,11 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
private let logger = Logger(subsystem: "com.beeminder.com", category: "GoalViewController")

let goal: Goal
private let healthStoreManager: HealthStoreManager
private let goalManager: GoalManager
private let requestManager: RequestManager
private let currentUserManager: CurrentUserManager
private let viewContext: NSManagedObjectContext

private let timeElapsedView = FreshnessIndicatorView()
fileprivate var goalImageView = GoalImageView(isThumbnail: false)
Expand All @@ -43,8 +51,18 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
// date corresponding to the datapoint to be created
private var date: Date = Date()

init(goal: Goal) {
init(goal: Goal,
healthStoreManager: HealthStoreManager,
goalManager: GoalManager,
requestManager: RequestManager,
currentUserManager: CurrentUserManager,
viewContext: NSManagedObjectContext) {
self.goal = goal
self.healthStoreManager = healthStoreManager
self.goalManager = goalManager
self.requestManager = requestManager
self.currentUserManager = currentUserManager
self.viewContext = viewContext
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -329,7 +347,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

@objc func timerButtonPressed() {
let controller = TimerViewController(goal: self.goal)
let controller = TimerViewController(goal: self.goal, requestManager: self.requestManager)
controller.modalPresentationStyle = .fullScreen
self.present(controller, animated: true, completion: nil)
}
Expand All @@ -338,11 +356,11 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
Task { @MainActor in
do {
if self.goal.isLinkedToHealthKit {
try await ServiceLocator.healthStoreManager.updateWithRecentData(goalID: self.goal.objectID, days: 7)
try await self.healthStoreManager.updateWithRecentData(goalID: self.goal.objectID, days: 7)
} else if goal.isDataProvidedAutomatically {
// 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 self.goalManager.forceAutodataRefresh(self.goal)
}
try await self.updateGoalAndInterface()
} catch {
Expand All @@ -369,7 +387,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
guard !self.goal.hideDataEntry else { return }
guard let existingDatapoint = datapoint as? DataPoint else { return }

let editDatapointViewController = EditDatapointViewController(goal: goal, datapoint: existingDatapoint)
let editDatapointViewController = EditDatapointViewController(goal: goal, datapoint: existingDatapoint, requestManager: self.requestManager, goalManager: self.goalManager)
let navigationController = UINavigationController(rootViewController: editDatapointViewController)
navigationController.modalPresentationStyle = .formSheet
self.present(navigationController, animated: true, completion: nil)
Expand Down Expand Up @@ -464,7 +482,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 self.requestManager.addDatapoint(urtext: self.urtext, slug: self.goal.slug)
self.commentTextField.text = ""

try await updateGoalAndInterface()
Expand All @@ -482,15 +500,15 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
}

do {
try await ServiceLocator.goalManager.refreshGoals()
try await self.goalManager.refreshGoals()
} catch {
logger.error("Failed up refresh goals after posting: \(error)")
}
}
}

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

Expand Down Expand Up @@ -582,9 +600,9 @@ private extension GoalViewController {
case goalStatistics
case goalSettings

func makeLink(username: String, goalName: String) -> URL? {
func makeLink(username: String, goalName: String, currentUserManager: CurrentUserManager) -> URL? {
guard
let accessToken = ServiceLocator.currentUserManager.accessToken
let accessToken = currentUserManager.accessToken
else { return nil }

let destinationUrl: URL
Expand Down Expand Up @@ -627,7 +645,7 @@ private extension GoalViewController {
let actions = options.map { option in
UIAction(title: option.title, image: UIImage(systemName: option.imageSystemName), handler: { [weak self] _ in
guard let self else { return }
guard let link = option.action.makeLink(username: self.goal.owner.username, goalName: self.goal.slug) else { return }
guard let link = option.action.makeLink(username: self.goal.owner.username, goalName: self.goal.slug, currentUserManager: self.currentUserManager) else { return }

let safariVC = SFSafariViewController(url: link)
safariVC.delegate = self
Expand Down
3 changes: 2 additions & 1 deletion BeeSwift/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
viewContext: ServiceLocator.persistentContainer.viewContext,
versionManager: ServiceLocator.versionManager,
goalManager: ServiceLocator.goalManager,
healthStoreManager: ServiceLocator.healthStoreManager
healthStoreManager: ServiceLocator.healthStoreManager,
requestManager: ServiceLocator.requestManager
)

let navigationController = UINavigationController(rootViewController: galleryVC)
Expand Down
17 changes: 14 additions & 3 deletions BeeSwift/Settings/ChooseHKMetricViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ class ChooseHKMetricViewController: UIViewController {
fileprivate let cellReuseIdentifier = "hkMetricTableViewCell"
fileprivate var tableView = UITableView()
let goal: Goal
private let healthStoreManager: HealthStoreManager
private let requestManager: RequestManager

init(goal: Goal) {
init(goal: Goal, healthStoreManager: HealthStoreManager, requestManager: RequestManager) {
self.goal = goal
self.healthStoreManager = healthStoreManager
self.requestManager = requestManager
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -131,14 +135,21 @@ extension ChooseHKMetricViewController : UITableViewDelegate, UITableViewDataSou
let metric = self.sortedMetricsByCategory[section]![indexPath.row]

do {
try await ServiceLocator.healthStoreManager.requestAuthorization(metric: metric)
try await self.healthStoreManager.requestAuthorization(metric: metric)
} catch {
logger.error("Error requesting permission for metric: \(error)")
self.tableView.isUserInteractionEnabled = true
return
}

self.navigationController?.pushViewController(ConfigureHKMetricViewController(goal: self.goal, metric: metric), animated: true)
self.navigationController?.pushViewController(
ConfigureHKMetricViewController(
goal: self.goal,
metric: metric,
healthStoreManager: self.healthStoreManager,
requestManager: self.requestManager
),
animated: true)
}
}

Expand Down
14 changes: 9 additions & 5 deletions BeeSwift/Settings/ConfigureHKMetricViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ class ConfigureHKMetricViewController : UIViewController {

private let goal: Goal
private let metric: HealthKitMetric
private let healthStoreManager: HealthStoreManager
private let requestManager: RequestManager

let previewDescriptionLabel = BSLabel()
fileprivate var datapointTableController = DatapointTableViewController()
fileprivate let noDataFoundLabel = BSLabel()
let saveButton = BSButton()

init(goal: Goal, metric : HealthKitMetric) {
init(goal: Goal, metric: HealthKitMetric, healthStoreManager: HealthStoreManager, requestManager: RequestManager) {
self.goal = goal
self.metric = metric
self.healthStoreManager = healthStoreManager
self.requestManager = requestManager
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -100,7 +104,7 @@ class ConfigureHKMetricViewController : UIViewController {

self.datapointTableController.hhmmformat = self.goal.hhmmFormat
Task { @MainActor in
let datapoints = try await self.metric.recentDataPoints(days: 5, deadline: self.goal.deadline, healthStore: ServiceLocator.healthStoreManager.healthStore)
let datapoints = try await self.metric.recentDataPoints(days: 5, deadline: self.goal.deadline, healthStore: self.healthStoreManager.healthStore)
self.datapointTableController.datapoints = datapoints

if datapoints.isEmpty {
Expand All @@ -115,7 +119,7 @@ class ConfigureHKMetricViewController : UIViewController {
}
}

let units = try await self.metric.units(healthStore: ServiceLocator.healthStoreManager.healthStore)
let units = try await self.metric.units(healthStore: self.healthStoreManager.healthStore)
unitsLabel.attributedText = {
let text = NSMutableAttributedString()
text.append(NSMutableAttributedString(string: "This metric reports results as ", attributes: [NSAttributedString.Key.font: UIFont.beeminder.defaultFont]))
Expand All @@ -142,7 +146,7 @@ class ConfigureHKMetricViewController : UIViewController {
self.goal.autodata = "apple"

do {
try await ServiceLocator.healthStoreManager.ensureUpdatesRegularly(goalID: self.goal.objectID)
try await self.healthStoreManager.ensureUpdatesRegularly(goalID: self.goal.objectID)
} catch {
logger.error("Error setting up goal \(error)")
hud.hide(animated: true)
Expand All @@ -157,7 +161,7 @@ class ConfigureHKMetricViewController : UIViewController {
params = ["ii_params" : ["name" : "apple", "metric" : self.goal.healthKitMetric!]]

do {
let _ = try await ServiceLocator.requestManager.put(url: "api/v1/users/{username}/goals/\(self.goal.slug).json", parameters: params)
let _ = try await self.requestManager.put(url: "api/v1/users/{username}/goals/\(self.goal.slug).json", parameters: params)
hud.mode = .customView
hud.customView = UIImageView(image: UIImage(systemName: "checkmark"))
hud.hide(animated: true, afterDelay: 2)
Expand Down
Loading

0 comments on commit 205c8d4

Please sign in to comment.