Skip to content

Commit 7571f7d

Browse files
authored
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.
1 parent c4ed232 commit 7571f7d

File tree

3 files changed

+119
-15
lines changed

3 files changed

+119
-15
lines changed

BeeSwift.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
9B65F2322CFA6427009674A7 /* DeeplinkGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */; };
1011
9B8CA57D24B120CA009C86C2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */; };
1112
A10D4E931B07948500A72D29 /* DatapointsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10D4E921B07948500A72D29 /* DatapointsTableView.swift */; };
1213
A10DC2DF207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */; };
@@ -217,6 +218,7 @@
217218
/* End PBXCopyFilesBuildPhase section */
218219

219220
/* Begin PBXFileReference section */
221+
9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkGenerator.swift; sourceTree = "<group>"; };
220222
9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
221223
A10D4E921B07948500A72D29 /* DatapointsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatapointsTableView.swift; sourceTree = "<group>"; };
222224
A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveHKMetricViewController.swift; sourceTree = "<group>"; };
@@ -476,6 +478,7 @@
476478
A196CB161AE4142E00B90A3E /* BeeSwift */ = {
477479
isa = PBXGroup;
478480
children = (
481+
9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */,
479482
A1E618E51E79E01900D8ED93 /* Cells */,
480483
E46071002B43DA7100305DB4 /* Gallery */,
481484
E46070FF2B43DA3D00305DB4 /* GoalView */,
@@ -1013,6 +1016,7 @@
10131016
A1E618E41E7934C700D8ED93 /* HealthKitConfigTableViewCell.swift in Sources */,
10141017
E4B083392932F90400A71564 /* ConfigureHKMetricViewController.swift in Sources */,
10151018
E43BEA842A036A9C00FC3A38 /* LogReader.swift in Sources */,
1019+
9B65F2322CFA6427009674A7 /* DeeplinkGenerator.swift in Sources */,
10161020
A196CB1F1AE4142F00B90A3E /* GalleryViewController.swift in Sources */,
10171021
A1BE73AA1E8B45BF00DEC4DB /* ChooseHKMetricViewController.swift in Sources */,
10181022
A149147B1BE79FD50060600A /* EditNotificationsViewController.swift in Sources */,

BeeSwift/DeeplinkGenerator.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// DeeplinkGenerator.swift
3+
// BeeSwift
4+
//
5+
// Created by krugerk on 2024-11-29.
6+
//
7+
8+
9+
struct DeeplinkGenerator {
10+
public static func generateDeepLinkToGoalCommitment(username: String, goalName: String) -> URL {
11+
URL(string: "https://www.beeminder.com/\(username)/\(goalName)#commitment")!
12+
}
13+
14+
public static func generateDeepLinkToGoalStop(username: String, goalName: String) -> URL {
15+
URL(string: "https://www.beeminder.com/\(username)/\(goalName)#stop")!
16+
}
17+
18+
public static func generateDeepLinkToGoalData(username: String, goalName: String) -> URL {
19+
URL(string: "https://www.beeminder.com/\(username)/\(goalName)#data")!
20+
}
21+
22+
public static func generateDeepLinkToGoalStatistics(username: String, goalName: String) -> URL {
23+
URL(string: "https://www.beeminder.com/\(username)/\(goalName)#statistics")!
24+
}
25+
26+
public static func generateDeepLinkToGoalSettings(username: String, goalName: String) -> URL {
27+
URL(string: "https://www.beeminder.com/\(username)/\(goalName)#settings")!
28+
}
29+
30+
public static func generateDeepLinkToUrl(accessToken: String, username: String, url: URL) -> URL {
31+
let baseUrlString = "https://www.beeminder.com/api/v1/users/\(username).json"
32+
33+
var components = URLComponents(string: baseUrlString)!
34+
35+
components.queryItems = [
36+
URLQueryItem(name: "access_token", value: accessToken),
37+
URLQueryItem(name: "redirect_to_url", value: url.absoluteString)
38+
]
39+
40+
return components.url!
41+
}
42+
}

BeeSwift/GoalViewController.swift

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,11 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
273273
make.right.equalTo(-sideMargin)
274274
}
275275
}
276-
277-
self.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(self.actionButtonPressed))]
276+
277+
let menuBarItem = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil)
278+
menuBarItem.menu = createGoalMenu()
279+
280+
self.navigationItem.rightBarButtonItems = [menuBarItem]
278281
if !self.goal.hideDataEntry {
279282
self.navigationItem.rightBarButtonItems?.append(UIBarButtonItem(image: UIImage(systemName: "stopwatch"), style: .plain, target: self, action: #selector(self.timerButtonPressed)))
280283
}
@@ -284,7 +287,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
284287
setValueTextField()
285288
updateInterfaceToMatchGoal()
286289
}
287-
290+
288291
override func viewDidLayoutSubviews() {
289292
// Ensure the submit button is always visible below the keyboard when interacting with
290293
// the submit datapoint controls
@@ -308,23 +311,13 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl
308311
}
309312
}
310313
}
311-
314+
312315
@objc func timerButtonPressed() {
313316
let controller = TimerViewController(goal: self.goal)
314317
controller.modalPresentationStyle = .fullScreen
315318
self.present(controller, animated: true, completion: nil)
316319
}
317-
318-
@objc func actionButtonPressed() {
319-
let username = goal.owner.username
320-
guard let accessToken = ServiceLocator.currentUserManager.accessToken,
321-
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 }
322-
323-
let safariVC = SFSafariViewController(url: viewGoalUrl)
324-
safariVC.delegate = self
325-
self.showDetailViewController(safariVC, sender: self)
326-
}
327-
320+
328321
@objc func refreshButtonPressed() {
329322
Task { @MainActor in
330323
do {
@@ -558,3 +551,68 @@ private extension DateFormatter {
558551
}
559552
}
560553
}
554+
555+
private extension GoalViewController {
556+
enum MenuAction {
557+
case goalCommitment
558+
case goalStop
559+
case goalData
560+
case goalStatistics
561+
case goalSettings
562+
563+
func makeLink(username: String, goalName: String) -> URL? {
564+
guard
565+
let accessToken = ServiceLocator.currentUserManager.accessToken
566+
else { return nil }
567+
568+
let destinationUrl: URL
569+
570+
switch self {
571+
case .goalCommitment:
572+
destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalCommitment(username: username, goalName: goalName)
573+
case .goalStop:
574+
destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalStop(username: username, goalName: goalName)
575+
case .goalData:
576+
destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalData(username: username, goalName: goalName)
577+
case .goalStatistics:
578+
destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalStatistics(username: username, goalName: goalName)
579+
case .goalSettings:
580+
destinationUrl = DeeplinkGenerator.generateDeepLinkToGoalSettings(username: username, goalName: goalName)
581+
}
582+
583+
return DeeplinkGenerator.generateDeepLinkToUrl(accessToken: accessToken, username: username, url: destinationUrl)
584+
}
585+
}
586+
587+
struct MenuOption {
588+
let title: String
589+
let action: MenuAction
590+
let imageSystemName: String
591+
}
592+
593+
private func getMenuOptions() -> [MenuOption] {
594+
[
595+
MenuOption(title: "Commitment", action: .goalCommitment, imageSystemName: "signature"),
596+
MenuOption(title: "Stop/Pause", action: .goalStop, imageSystemName: "pause.fill"),
597+
MenuOption(title: "Data", action: .goalData, imageSystemName: "tablecells"),
598+
MenuOption(title: "Statistics", action: .goalStatistics, imageSystemName: "chart.bar.fill"),
599+
MenuOption(title: "Settings", action: .goalSettings, imageSystemName: "gearshape.2"),
600+
]
601+
}
602+
603+
private func createGoalMenu() -> UIMenu {
604+
let options = getMenuOptions()
605+
let actions = options.map { option in
606+
UIAction(title: option.title, image: UIImage(systemName: option.imageSystemName), handler: { [weak self] _ in
607+
guard let self else { return }
608+
guard let link = option.action.makeLink(username: self.goal.owner.username, goalName: self.goal.slug) else { return }
609+
610+
let safariVC = SFSafariViewController(url: link)
611+
safariVC.delegate = self
612+
self.showDetailViewController(safariVC, sender: self)
613+
})
614+
}
615+
616+
return UIMenu(title: "bmndr.com/\(goal.owner.username)/\(goal.slug)", children: actions)
617+
}
618+
}

0 commit comments

Comments
 (0)