Skip to content

Commit

Permalink
introduces goal menu with deep links to goal on website (#540)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
krugerk authored Dec 26, 2024
1 parent c4ed232 commit 7571f7d
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 15 deletions.
4 changes: 4 additions & 0 deletions BeeSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -217,6 +218,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkGenerator.swift; sourceTree = "<group>"; };
9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; 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>"; };
Expand Down Expand Up @@ -476,6 +478,7 @@
A196CB161AE4142E00B90A3E /* BeeSwift */ = {
isa = PBXGroup;
children = (
9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */,
A1E618E51E79E01900D8ED93 /* Cells */,
E46071002B43DA7100305DB4 /* Gallery */,
E46070FF2B43DA3D00305DB4 /* GoalView */,
Expand Down Expand Up @@ -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 */,
Expand Down
42 changes: 42 additions & 0 deletions BeeSwift/DeeplinkGenerator.swift
Original file line number Diff line number Diff line change
@@ -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!
}
}
88 changes: 73 additions & 15 deletions BeeSwift/GoalViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}

0 comments on commit 7571f7d

Please sign in to comment.