Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update iOS project version and add recreation activities menu item #84

Merged
merged 6 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions apps/ios/GuideDogs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -6472,7 +6472,7 @@
CODE_SIGN_ENTITLEMENTS = GuideDogs/Assets/PropertyLists/SoundscapeDF.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = "";
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES;
ENABLE_BITCODE = NO;
Expand Down Expand Up @@ -6503,7 +6503,7 @@
"$(inherited)",
"$(PROJECT_DIR)/GuideDogs",
);
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) -DADHOC";
PRODUCT_BUNDLE_IDENTIFIER = "services.soundscape-adhoc";
Expand Down Expand Up @@ -6533,6 +6533,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = X4H33NKGKY;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
Expand Down Expand Up @@ -6567,6 +6568,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = X4H33NKGKY;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
Expand Down Expand Up @@ -6598,6 +6600,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = X4H33NKGKY;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
Expand Down Expand Up @@ -6758,7 +6761,7 @@
CODE_SIGN_ENTITLEMENTS = GuideDogs/Assets/PropertyLists/Soundscape.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 19;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = X4H33NKGKY;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES;
Expand Down Expand Up @@ -6790,7 +6793,7 @@
"$(inherited)",
"$(PROJECT_DIR)/GuideDogs",
);
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) -DDEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "services.soundscape-debug";
Expand All @@ -6817,11 +6820,9 @@
CLANG_STATIC_ANALYZER_MODE = deep;
CODE_SIGN_ENTITLEMENTS = GuideDogs/Assets/PropertyLists/Soundscape.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = X4H33NKGKY;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = X4H33NKGKY;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES;
ENABLE_BITCODE = NO;
FILE_SHARING_ENABLED = NO;
Expand Down Expand Up @@ -6850,13 +6851,12 @@
"$(inherited)",
"$(PROJECT_DIR)/GuideDogs",
);
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) -DRELEASE";
PRODUCT_BUNDLE_IDENTIFIER = services.soundscape;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore services.soundscape 1691940011";
RUN_CLANG_STATIC_ANALYZER = YES;
SDKROOT = iphoneos;
SWIFT_OBJC_BRIDGING_HEADER = "GuideDogs/Code/App/Soundscape-Bridging-Header.h";
Expand Down
3 changes: 1 addition & 2 deletions apps/ios/GuideDogs/Assets/PropertyLists/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<key>CFBundleSpokenName</key>
<string>${BUNDLE_SPOKEN_NAME}</string>
<key>CFBundleVersion</key>
<string>7</string>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
Expand Down Expand Up @@ -156,7 +156,6 @@
<string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct UniversalLinkComponents {
// Add query items
components.queryItems = queryItems

return components.url
return components.url
}

// MARK: Initialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ extension AuthoredActivityContent {
///
/// - Parameter gpx: A parsed GPX file
/// - Returns: An ``AuthoredActivityContent``, or `nil` if parsing failed or required properties were missing. Currently, waypoints or POIs may be skipped if they lack coordinate data.
static func parse(gpx: GPXRoot) -> AuthoredActivityContent? {
static func parse(gpx: GPXRoot, baseURL: URL) -> AuthoredActivityContent? {
guard let metadata = gpx.metadata else {
return nil
}
Expand All @@ -206,15 +206,15 @@ extension AuthoredActivityContent {

var imageURL: URL?
if let image = metadata.links.first, image.mimetype?.hasPrefix("image") != nil, let href = image.href {
imageURL = URL(string: href)
imageURL = URL(string: href, relativeTo: baseURL)
}

// Parse the waypoints and POIs based on the file version
switch ext.version ?? "1" {
case "1":
// Version 1 just uses all the top-level waypoints `<wpt></wpt>` defined in the GPX, in order

let wpts: [ActivityWaypoint] = waypoints(from: gpx.waypoints)
let wpts: [ActivityWaypoint] = waypoints(from: gpx.waypoints, baseURL: baseURL)

// For waypoints in this experience, require names, descriptions, and street addresses
guard !wpts.isEmpty, !wpts.contains(where: { $0.name == nil }) else {
Expand Down Expand Up @@ -242,7 +242,7 @@ extension AuthoredActivityContent {
}

// Waypoints are strict about requiring names and locations
let wpts: [ActivityWaypoint] = waypoints(from: route.points)
let wpts: [ActivityWaypoint] = waypoints(from: route.points, baseURL: baseURL)

// For waypoints in this experience, require names, descriptions, and street addresses
guard !wpts.isEmpty, !wpts.contains(where: { $0.name == nil }) else {
Expand Down Expand Up @@ -279,28 +279,33 @@ extension AuthoredActivityContent {
///
/// - Parameter waypoints: an array of ``GPXWaypoint``s
/// - Returns: an array of ``ActivityWaypoint``s including annotation data (if applicable)
private static func waypoints(from waypoints: [GPXWaypoint]) -> [ActivityWaypoint] {
private static func waypoints(from waypoints: [GPXWaypoint], baseURL: URL) -> [ActivityWaypoint] {
let imageMimeTypes = Set(["image/jpeg", "image/jpg", "image/png"])
let audioMimeTypes = Set(["audio/mpeg", "audio/x-m4a"])

return waypoints.compactMap { wpt in
let links: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({
let imageLinks: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({
guard let mimetype = $0.mimetype else { return false }
return imageMimeTypes.contains(mimetype)
}) ?? []

let parsedImages: [ActivityWaypointImage] = links.compactMap { link in
let parsedImages: [ActivityWaypointImage] = imageLinks.compactMap { link in
guard let href = link.href,
let url = URL(string: href) else {
let url = URL(string: href, relativeTo: baseURL) else {
return nil
}

return ActivityWaypointImage(url: url, altText: link.text)
}

let parsedAudioClips: [ActivityWaypointAudioClip] = links.compactMap { link in
let audioLinks: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({
guard let mimetype = $0.mimetype else { return false }
return audioMimeTypes.contains(mimetype)
}) ?? []

let parsedAudioClips: [ActivityWaypointAudioClip] = audioLinks.compactMap { link in
guard let href = link.href,
let url = URL(string: href) else {
let url = URL(string: href, relativeTo: baseURL) else {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,15 @@ class AuthoredActivityLoader {
return nil
}

guard let index = knownActivities.events.firstIndex(where: { activityID == $0.id }),
let baseURL = knownActivities.events[index].downloadPath else {
GDLogAppError("Unable to find download path for activity with ID: \(activityID)")
return nil
}


// Parse the GPX file and validate its contents
return AuthoredActivityContent.parse(gpx: gpx)
return AuthoredActivityContent.parse(gpx: gpx, baseURL: baseURL)
}

func add(_ activityID: String, linkVersion: UniversalLinkVersion) async throws {
Expand Down Expand Up @@ -296,8 +303,8 @@ class AuthoredActivityLoader {
throw ActivityLoaderError.unableToLoadContent
}

guard let content = AuthoredActivityContent.parse(gpx: gpx) else {
GDLogWarn(.routeGuidance, "Unable to parse activity content from GPX for \(id)")
guard let content = AuthoredActivityContent.parse(gpx: gpx, baseURL: metadata.downloadPath!) else {
GDLogWarn(.routeGuidance, "Unable to parse activity content from GPX for \(id), URL = \(metadata.downloadPath)")

NotificationCenter.default.post(name: .didTryActivityUpdate, object: self, userInfo: [
Keys.updateSuccess: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,18 @@ struct AuthoredActivityMetadata: Codable, CustomStringConvertible {
}

/// Builds the remote server path that the content can be downloaded from
/// E.G. https://share.openscape.io/experiences/some-activity.gpx
/// E.G. https://share.soundscape.services/experiences/<id>/activity.gpx
var downloadPath: URL? {
var components = URLComponents()
var components = URLComponents()

switch linkVersion {
case .v1:
components.scheme = "https"
components.host = "share.openscape.io"
components.path = "/experiences/\(id).gpx"
case .v2, .v3:
// Version 2 and 3 links also look the same (perk of forking)
components.scheme = "https"
components.host = "share.soundscape.services"
components.path = "experiences/\(id).gpx"
components.path = "/activities/\(id)/activity.gpx"
default:
// no other versions currently supported
break
Comment on lines +57 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why there wasn't a default case before. I'm not sure what Swift semantics are for unhandled cases, but the behavior with the default case (return an empty URL) seems correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all cases were handled explicitly before. Swift does require all cases to be handled either explicitly or with a default case.

I was thinking of proposing a v4 link in the future that would contain the full URL to the activity.gpx file. That would allow the authoring tool to be used on other domains for development etc.

}

return components.url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import UIKit
import SafariServices

enum MenuItem {
case home, devices, help, settings, status, feedback, rate, share
case home, recreation, devices, help, settings, status, feedback, rate, share

var localizedString: String {
switch self {
case .home: return GDLocalizedString("ui.menu.close")
case .recreation: return GDLocalizedString("menu.events")
case .devices: return GDLocalizedString("menu.devices")
case .help: return GDLocalizedString("menu.help_and_tutorials")
case .settings: return GDLocalizedString("settings.screen_title")
Expand All @@ -28,6 +29,7 @@ enum MenuItem {
var accessibilityString: String {
switch self {
case .home: return GDLocalizedString("ui.menu.close")
case .recreation: return GDLocalizedString("menu.events")
case .devices: return GDLocalizedString("menu.devices")
case .help: return GDLocalizedString("menu.help_and_tutorials")
case .settings: return GDLocalizedString("settings.screen_title")
Expand All @@ -41,6 +43,7 @@ enum MenuItem {
var icon: UIImage? {
switch self {
case .home: return UIImage(named: "ic_chevron_left_28px")
case .recreation: return UIImage(named: "nordic_walking_white_28dp")
case .devices: return UIImage(named: "baseline-headset-28px")
case .help: return UIImage(named: "ic_help_outline_28px")
case .settings: return UIImage(named: "ic_settings_28px")
Expand All @@ -61,6 +64,7 @@ class MenuViewController: UIViewController {
override func loadView() {
// Build views for menu items
menuView.addMenuItem(.devices)
menuView.addMenuItem(.recreation)
menuView.addMenuItem(.settings)
menuView.addMenuItem(.help)
menuView.addMenuItem(.feedback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class HomeViewController: UIViewController {
/// - Returns: The segue associated with this menu item
static func segue(for menuItem: MenuItem) -> String? {
switch menuItem {
case .recreation: return Segue.showRecreationActivities
case .devices: return Segue.showManageDevices
case .help: return Segue.showHelp
case .settings: return Segue.showSettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class AuthoredActivityContentTest: XCTestCase {

// MARK: Test GPX Parsing

static let baseURL = URL(string: "https://example.com")!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is extraneous, though probably harmless.

/// Tests parsing from GPX
/// Using `GPXSoundscapeSharedContentExtensions` v1
/// And minimal other details
Expand Down Expand Up @@ -63,7 +64,7 @@ final class AuthoredActivityContentTest: XCTestCase {
XCTFail("Failed to get parsedData")
return
}
guard let activity = AuthoredActivityContent.parse(gpx: root) else {
guard let activity = AuthoredActivityContent.parse(gpx: root, baseURL: AuthoredActivityContentTest.baseURL) else {
XCTFail("Failed to create AuthoredActivityContent from GPXRoot")
return
}
Expand Down Expand Up @@ -150,7 +151,7 @@ final class AuthoredActivityContentTest: XCTestCase {
XCTFail("Failed to get parsedData")
return
}
guard let activity = AuthoredActivityContent.parse(gpx: root) else {
guard let activity = AuthoredActivityContent.parse(gpx: root, baseURL: AuthoredActivityContentTest.baseURL) else {
XCTFail("Failed to create AuthoredActivityContent from GPXRoot")
return
}
Expand Down
Loading