From 7571f7de138fc04589e37a5fe588b9b221c4573c Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Thu, 26 Dec 2024 05:09:32 +0100 Subject: [PATCH] introduces goal menu with deep links to goal on website (#540) ## Summary The goal screen contained an action button which opened the goal in an in-app browser. This way one could manipulate various aspects of the goal or view data (statistics) from on the website as these features are lacking in the app. This merge request replaces the action button on the goal screen, with single link to the goal page on the website, with a menu with links to all of the main sections of the website's rendition of the goal: commitment, stop/pause, data, statistics, and settings. This makes it easier to more quickly arrive at a particular section. It might also increase feature discoverability. For example, the delta text was removed from the app and the current workaround to check "how much to do to earn x days off" is available on the website in the statistics section under the "Amounts Due By Day" subsection. Also, the app does not support features such as setting the goal's description or title. It also does not allow editing of datapoints of goals with autodata whereas the website does. *For UI changes including screenshots of before and after is great.* ## before Tapping the action button opens beeminder.com/user/goal in an in-app browser after which one can navigate through the sections of the goal on the webpage presented. ## Validation Opened the app in the simulator. Clicked through various goals and the corresponding "open this section of the goal on the website" links. --- BeeSwift.xcodeproj/project.pbxproj | 4 ++ BeeSwift/DeeplinkGenerator.swift | 42 ++++++++++++++ BeeSwift/GoalViewController.swift | 88 +++++++++++++++++++++++++----- 3 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 BeeSwift/DeeplinkGenerator.swift diff --git a/BeeSwift.xcodeproj/project.pbxproj b/BeeSwift.xcodeproj/project.pbxproj index b2d68d650..8ae2d59bc 100644 --- a/BeeSwift.xcodeproj/project.pbxproj +++ b/BeeSwift.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 9B65F2322CFA6427009674A7 /* DeeplinkGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */; }; 9B8CA57D24B120CA009C86C2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */; }; A10D4E931B07948500A72D29 /* DatapointsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10D4E921B07948500A72D29 /* DatapointsTableView.swift */; }; A10DC2DF207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */; }; @@ -217,6 +218,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkGenerator.swift; sourceTree = ""; }; 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; 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 = ""; }; @@ -476,6 +478,7 @@ A196CB161AE4142E00B90A3E /* BeeSwift */ = { isa = PBXGroup; children = ( + 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */, A1E618E51E79E01900D8ED93 /* Cells */, E46071002B43DA7100305DB4 /* Gallery */, E46070FF2B43DA3D00305DB4 /* GoalView */, @@ -1013,6 +1016,7 @@ A1E618E41E7934C700D8ED93 /* HealthKitConfigTableViewCell.swift in Sources */, E4B083392932F90400A71564 /* ConfigureHKMetricViewController.swift in Sources */, E43BEA842A036A9C00FC3A38 /* LogReader.swift in Sources */, + 9B65F2322CFA6427009674A7 /* DeeplinkGenerator.swift in Sources */, A196CB1F1AE4142F00B90A3E /* GalleryViewController.swift in Sources */, A1BE73AA1E8B45BF00DEC4DB /* ChooseHKMetricViewController.swift in Sources */, A149147B1BE79FD50060600A /* EditNotificationsViewController.swift in Sources */, diff --git a/BeeSwift/DeeplinkGenerator.swift b/BeeSwift/DeeplinkGenerator.swift new file mode 100644 index 000000000..2aba61547 --- /dev/null +++ b/BeeSwift/DeeplinkGenerator.swift @@ -0,0 +1,42 @@ +// +// DeeplinkGenerator.swift +// BeeSwift +// +// Created by krugerk on 2024-11-29. +// + + +struct DeeplinkGenerator { + public static func generateDeepLinkToGoalCommitment(username: String, goalName: String) -> URL { + URL(string: "https://www.beeminder.com/\(username)/\(goalName)#commitment")! + } + + public static func generateDeepLinkToGoalStop(username: String, goalName: String) -> URL { + URL(string: "https://www.beeminder.com/\(username)/\(goalName)#stop")! + } + + public static func generateDeepLinkToGoalData(username: String, goalName: String) -> URL { + URL(string: "https://www.beeminder.com/\(username)/\(goalName)#data")! + } + + public static func generateDeepLinkToGoalStatistics(username: String, goalName: String) -> URL { + URL(string: "https://www.beeminder.com/\(username)/\(goalName)#statistics")! + } + + public static func generateDeepLinkToGoalSettings(username: String, goalName: String) -> URL { + URL(string: "https://www.beeminder.com/\(username)/\(goalName)#settings")! + } + + public static func generateDeepLinkToUrl(accessToken: String, username: String, url: URL) -> URL { + let baseUrlString = "https://www.beeminder.com/api/v1/users/\(username).json" + + var components = URLComponents(string: baseUrlString)! + + components.queryItems = [ + URLQueryItem(name: "access_token", value: accessToken), + URLQueryItem(name: "redirect_to_url", value: url.absoluteString) + ] + + return components.url! + } +} diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift index a07f27a26..6c1fa056b 100644 --- a/BeeSwift/GoalViewController.swift +++ b/BeeSwift/GoalViewController.swift @@ -273,8 +273,11 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl make.right.equalTo(-sideMargin) } } - - self.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(self.actionButtonPressed))] + + let menuBarItem = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil) + menuBarItem.menu = createGoalMenu() + + self.navigationItem.rightBarButtonItems = [menuBarItem] if !self.goal.hideDataEntry { self.navigationItem.rightBarButtonItems?.append(UIBarButtonItem(image: UIImage(systemName: "stopwatch"), style: .plain, target: self, action: #selector(self.timerButtonPressed))) } @@ -284,7 +287,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl setValueTextField() updateInterfaceToMatchGoal() } - + override func viewDidLayoutSubviews() { // Ensure the submit button is always visible below the keyboard when interacting with // the submit datapoint controls @@ -308,23 +311,13 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl } } } - + @objc func timerButtonPressed() { let controller = TimerViewController(goal: self.goal) controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } - - @objc func actionButtonPressed() { - let username = goal.owner.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 safariVC = SFSafariViewController(url: viewGoalUrl) - safariVC.delegate = self - self.showDetailViewController(safariVC, sender: self) - } - + @objc func refreshButtonPressed() { Task { @MainActor in do { @@ -558,3 +551,68 @@ private extension DateFormatter { } } } + +private extension GoalViewController { + enum MenuAction { + case goalCommitment + case goalStop + case goalData + case goalStatistics + case goalSettings + + func makeLink(username: String, goalName: String) -> URL? { + guard + let accessToken = ServiceLocator.currentUserManager.accessToken + else { return nil } + + let destinationUrl: URL + + switch self { + case .goalCommitment: + destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalCommitment(username: username, goalName: goalName) + case .goalStop: + destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalStop(username: username, goalName: goalName) + case .goalData: + destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalData(username: username, goalName: goalName) + case .goalStatistics: + destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalStatistics(username: username, goalName: goalName) + case .goalSettings: + destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalSettings(username: username, goalName: goalName) + } + + return DeeplinkGenerator.generateDeepLinkToUrl(accessToken: accessToken, username: username, url: destinationUrl) + } + } + + struct MenuOption { + let title: String + let action: MenuAction + let imageSystemName: String + } + + private func getMenuOptions() -> [MenuOption] { + [ + MenuOption(title: "Commitment", action: .goalCommitment, imageSystemName: "signature"), + MenuOption(title: "Stop/Pause", action: .goalStop, imageSystemName: "pause.fill"), + MenuOption(title: "Data", action: .goalData, imageSystemName: "tablecells"), + MenuOption(title: "Statistics", action: .goalStatistics, imageSystemName: "chart.bar.fill"), + MenuOption(title: "Settings", action: .goalSettings, imageSystemName: "gearshape.2"), + ] + } + + private func createGoalMenu() -> UIMenu { + let options = getMenuOptions() + 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 } + + let safariVC = SFSafariViewController(url: link) + safariVC.delegate = self + self.showDetailViewController(safariVC, sender: self) + }) + } + + return UIMenu(title: "bmndr.com/\(goal.owner.username)/\(goal.slug)", children: actions) + } +}