Skip to content

ADD - Event export possibility #17

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
20 changes: 19 additions & 1 deletion Workout Core/Model/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ private enum PreferenceKeys: String, KeyValueStoreKey {
case maxHeartRate = "maxHeartRate"
case runningHeartZones = "runningHeartZones"
case exportRouteType = "exportRouteType"
case defaultCalendar = "defaultCalendar"

case reviewRequestCounter = "reviewRequestCounter"

Expand All @@ -34,6 +35,7 @@ public protocol PreferencesDelegate: AnyObject {
@objc optional func stepSourceChanged()
@objc optional func runningHeartZonesConfigChanged()
@objc optional func routeTypeChanged()
@objc optional func defaultCalendarChanged()

@objc optional func reviewCounterUpdated()

Expand All @@ -42,7 +44,7 @@ public protocol PreferencesDelegate: AnyObject {
public class Preferences {

private enum Change {
case generic, systemOfUnits, stepSource, hzConfig, reviewCounter, routeType
case generic, systemOfUnits, stepSource, hzConfig, reviewCounter, routeType, defaultCalendar
}

public let local = KeyValueStore(userDefaults: UserDefaults.standard)
Expand Down Expand Up @@ -74,6 +76,8 @@ public class Preferences {
d.reviewCounterUpdated?()
case .routeType:
d.routeTypeChanged?()
case .defaultCalendar:
d.defaultCalendarChanged?()
case .generic:
d.preferencesChanged?()
}
Expand Down Expand Up @@ -159,4 +163,18 @@ public class Preferences {
}
}

public var defaultCalendarSelected: String {
get {
return local.string(forKey: PreferenceKeys.defaultCalendar) ?? ""
}
set {
if newValue == "" {
local.removeObject(forKey: PreferenceKeys.defaultCalendar)
} else {
local.set(newValue, forKey: PreferenceKeys.defaultCalendar)
}
saveChanges(.defaultCalendar)
}
}

}
166 changes: 166 additions & 0 deletions Workout Core/Model/Workout/Workout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import HealthKit
import MBLibrary
import EventKit

public protocol WorkoutDelegate: AnyObject {

Expand Down Expand Up @@ -568,4 +569,169 @@ public class Workout: Equatable {
}
}

private let typeRow = 0
private let startRow = 1
private let endRow = 2
private let durationRow = 3
private var distanceRow: Int? {
guard self.totalDistance != nil else {
return nil
}

return 1 + durationRow
}
private var avgHeartRow: Int? {
guard self.avgHeart != nil else {
return nil
}

return 1 + (distanceRow ?? durationRow)
}
private var maxHeartRow: Int? {
guard self.maxHeart != nil else {
return nil
}

let base = [avgHeartRow, distanceRow].lazy.compactMap { $0 }.first ?? durationRow
return 1 + base
}
private var paceRow: Int? {
guard self.pace != nil else {
return nil
}

let base = [maxHeartRow, avgHeartRow, distanceRow].lazy.compactMap { $0 }.first ?? durationRow
return 1 + base
}
private var speedRow: Int? {
guard self.speed != nil else {
return nil
}

let base = [paceRow, maxHeartRow, avgHeartRow, distanceRow].lazy.compactMap { $0 }.first ?? durationRow
return 1 + base
}
private var energyRow: Int? {
guard self.totalEnergy != nil else {
return nil
}

let base = [speedRow, paceRow, maxHeartRow, avgHeartRow, distanceRow].lazy.compactMap { $0 }.first ?? durationRow
return 1 + base
}
private var elevationRow: Int? {
let (asc, desc) = self.elevationChange
guard asc != nil || desc != nil else {
return nil
}

let base = [energyRow, speedRow, paceRow, maxHeartRow, avgHeartRow, distanceRow].lazy.compactMap { $0 }.first ?? durationRow
return 1 + base
}
Comment on lines +572 to +630
Copy link
Owner

Choose a reason for hiding this comment

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

If I'm not mistaken these are just copied from what WorkoutTVC uses to configure the first section. Instead of duplicating the code why not making these public and let WorkoutTVC use these directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, but maybe I should create a new class for it like "WorkoutData", that will be used by the WorkoutTVC and Workout classes to access to theses data. What do you think?

Copy link
Owner

Choose a reason for hiding this comment

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

I'm not sure about this since Workout is already a wrapper for a HKWorkout and the model for WorkoutTVC.
A possible direction could be creating a sub-class for Workout called something like Workout.DataIndex (holding a weak reference to the workout in order to access what is now self.avgHeart and the like) exposing this properties and maybe the count of how many are non nil.
We could make the init private so only the workout itself can initialize it.


public func ICSexport(for systemOfUnits: SystemOfUnits, _ callback: @escaping (URL?) -> Void) {
guard isLoaded, !hasError,
EKEventStore.authorizationStatus(for: EKEntityType.event) != EKAuthorizationStatus.denied,
EKEventStore.authorizationStatus(for: EKEntityType.event) != EKAuthorizationStatus.restricted
else {
callback(nil)
Copy link
Owner

Choose a reason for hiding this comment

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

This is not a real failure, we should prompt the user to update the authorization for calendar access

return
}

DispatchQueue.background.async {
let eventStore = EKEventStore()

var description = ""
let nbLines = [self.typeRow, self.startRow, self.endRow, self.durationRow, self.distanceRow, self.avgHeartRow, self.maxHeartRow, self.paceRow, self.speedRow, self.energyRow, self.elevationRow].lazy.compactMap { $0 }.count
for n in 0...nbLines {
switch n {
case self.typeRow:
description += NSLocalizedString("WRKT_TYPE", comment: "Type")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += self.name + "\n"
case self.startRow:
description += NSLocalizedString("WRKT_START", comment: "Start")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += self.startDate.formattedDateTime + "\n"
case self.endRow:
description += NSLocalizedString("WRKT_END", comment: "End")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += self.endDate.formattedDateTime + "\n"
case self.durationRow:
description += NSLocalizedString("WRKT_DURATION", comment: "Duration")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += self.duration.formattedDuration + "\n"
case self.distanceRow:
description += NSLocalizedString("WRKT_DISTANCE", comment: "Distance")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += (self.totalDistance?.formatAsDistance(withUnit: self.distanceUnit.unit(for: systemOfUnits)) ?? missingValueStr) + "\n"
case self.avgHeartRow:
description += NSLocalizedString("WRKT_AVG_HEART", comment: "Average Heart Rate")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += (self.avgHeart?.formatAsHeartRate(withUnit: WorkoutUnit.heartRate.unit(for: systemOfUnits)) ?? missingValueStr) + "\n"
case self.maxHeartRow:
description += NSLocalizedString("WRKT_MAX_HEART", comment: "Max Heart Rate")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += (self.maxHeart?.formatAsHeartRate(withUnit: WorkoutUnit.heartRate.unit(for: systemOfUnits)) ?? missingValueStr) + "\n"
case self.paceRow:
description += NSLocalizedString("WRKT_AVG_PACE", comment: "Average Pace")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += (self.pace?.formatAsPace(withReferenceLength: self.paceUnit.unit(for: systemOfUnits)) ?? missingValueStr) + "\n"
case self.speedRow:
description += NSLocalizedString("WRKT_AVG_SPEED", comment: "Average Speed")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
description += (self.speed?.formatAsSpeed(withUnit: self.speedUnit.unit(for: systemOfUnits)) ?? missingValueStr) + "\n"
case self.energyRow:
description += NSLocalizedString("WRKT_ENERGY", comment: "Energy")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")
if let total = self.totalEnergy {
if let active = self.activeEnergy {
description += String(format: NSLocalizedString("WRKT_SPLIT_CAL_%@_TOTAL_%@", comment: "Active/Total"),
active.formatAsEnergy(withUnit: WorkoutUnit.calories.unit(for: systemOfUnits)),
total.formatAsEnergy(withUnit: WorkoutUnit.calories.unit(for: systemOfUnits))) + "\n"
} else {
description += total.formatAsEnergy(withUnit: WorkoutUnit.calories.unit(for: systemOfUnits)) + "\n"
}
} else {
description += missingValueStr + "\n"
}
case self.elevationRow:
description += NSLocalizedString("WRKT_ELEVATION", comment: "Elevation Change")
description += NSLocalizedString("WRKT_EVENT_SEPARATOR", comment: "Separator")

let (asc, desc) = self.elevationChange
for (v, dir) in [(asc, "↗ "), (desc, "↘ ")] {
guard let v = v else {
continue
}

description += dir + v.formatAsElevationChange(withUnit: WorkoutUnit.elevation.unit(for: systemOfUnits)) + "\n"
}
default:
break
}
}
description.removeLast()

eventStore.requestAccess(to: .event, completion: { (granted, error) in
Copy link
Owner

@piscoTech piscoTech Jan 3, 2020

Choose a reason for hiding this comment

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

I think authorization to use the calendar should be handled by the main app, not Workout Core, like it's currently done for HealthKit. Also the string containing the reason we use the calendar should be updated to reflect the new functionality, the current value is just there to satisfy a requirement from Apple but never actually used

if (granted) && (error == nil) {
let event = EKEvent(eventStore: eventStore)

event.title = "\(self.name) - \(self.duration.formattedDuration)"
event.startDate = self.startDate
event.endDate = self.endDate
event.notes = description
event.calendar = eventStore.calendar(withIdentifier: Preferences().defaultCalendarSelected) ?? eventStore.defaultCalendarForNewEvents
do {
try eventStore.save(event, span: .thisEvent)
} catch let e as NSError {
print(e)
callback(nil)
return
}
}
})
callback(URL(string: "calshow:\(self.startDate.timeIntervalSinceReferenceDate)")!)
}
}
}
8 changes: 8 additions & 0 deletions Workout.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
3B598F2223BD1B7D0046A5FC /* SelectCalendarTVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B598F2123BD1B7D0046A5FC /* SelectCalendarTVC.swift */; };
3B598F2A23BD1BA70046A5FC /* CalendarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B598F2923BD1BA70046A5FC /* CalendarTableViewCell.swift */; };
6FDD7897C9603553538BAE3D /* Pods_Workout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36DFDE010077F29DA04CBBB5 /* Pods_Workout.framework */; };
7A09205922EB8BA900EC86A6 /* CyclingWorkout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09205822EB8BA900EC86A6 /* CyclingWorkout.swift */; };
7A1CE14122C7649200414497 /* WorkoutBulkExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1CE14022C7649200414497 /* WorkoutBulkExporter.swift */; };
Expand Down Expand Up @@ -156,6 +158,8 @@
/* Begin PBXFileReference section */
15DCB1062DF12CD614F251B6 /* Pods-Workout.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Workout.release.xcconfig"; path = "Pods/Target Support Files/Pods-Workout/Pods-Workout.release.xcconfig"; sourceTree = "<group>"; };
36DFDE010077F29DA04CBBB5 /* Pods_Workout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Workout.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B598F2123BD1B7D0046A5FC /* SelectCalendarTVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCalendarTVC.swift; sourceTree = "<group>"; };
3B598F2923BD1BA70046A5FC /* CalendarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTableViewCell.swift; sourceTree = "<group>"; };
3BD3437423A998CC00567BD7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
3BD3437623A998CC00567BD7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
3BD3437823A998D800567BD7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -327,6 +331,7 @@
7A650B5322C1354300059687 /* LoadMoreCell.swift */,
7A9BF6D322CA317600EC1D95 /* FilterCells.swift */,
7AE6A94122E793E600D2ED88 /* WorkoutGeneralDataCell.swift */,
3B598F2923BD1BA70046A5FC /* CalendarTableViewCell.swift */,
);
path = View;
sourceTree = "<group>";
Expand All @@ -339,6 +344,7 @@
7A650B4522C1354000059687 /* StepSourceTVC.swift */,
7A650B4422C1354000059687 /* RunningHeartZonesTVC.swift */,
7A1F8CC123AF5610008EA2E9 /* RouteTypeTVC.swift */,
3B598F2123BD1B7D0046A5FC /* SelectCalendarTVC.swift */,
);
path = Settings;
sourceTree = "<group>";
Expand Down Expand Up @@ -831,10 +837,12 @@
7A650B6822C1354500059687 /* ListTVC.swift in Sources */,
7A9BF6D422CA317600EC1D95 /* FilterCells.swift in Sources */,
7A650B5D22C1354500059687 /* UnitsTVC.swift in Sources */,
3B598F2223BD1B7D0046A5FC /* SelectCalendarTVC.swift in Sources */,
7A1F8CC223AF5610008EA2E9 /* RouteTypeTVC.swift in Sources */,
7A9BF6CC22CA233900EC1D95 /* Extensions.swift in Sources */,
7A650B6922C1354500059687 /* AboutVC.swift in Sources */,
7A650B5F22C1354500059687 /* RunningHeartZonesTVC.swift in Sources */,
3B598F2A23BD1BA70046A5FC /* CalendarTableViewCell.swift in Sources */,
7A650B0322C1300100059687 /* SceneDelegate.swift in Sources */,
7A650B6622C1354500059687 /* FilterListTVC.swift in Sources */,
);
Expand Down
4 changes: 4 additions & 0 deletions Workout/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@
"REMOVE_ADS" = "Remove Ads";
"MANAGE_CONSENT" = "Manage Ads Consent";
"MANAGE_CONSENT_ERR" = "Unable to update ads consent";

// MARK: - Event Export

"WRKT_EVENT_SEPARATOR" = ": ";
28 changes: 17 additions & 11 deletions Workout/Controller/ListTVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko
private let list = WorkoutList(healthData: healthData, preferences: preferences)
private var exporter: WorkoutBulkExporter?

private var standardRightBtn: UIBarButtonItem!
private let refreshC = UIRefreshControl()

private var standardRightBtns: [UIBarButtonItem]!
private var standardLeftBtn: UIBarButtonItem!
@IBOutlet private weak var enterExportModeBtn: UIBarButtonItem!

Expand Down Expand Up @@ -49,13 +51,18 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko
navigationItem.titleView = titleView
navBar?.enhancedDelegate = self

standardRightBtn = navigationItem.rightBarButtonItem
standardRightBtns = navigationItem.rightBarButtonItems
standardLeftBtn = navigationItem.leftBarButtonItem
if #available(iOS 13, *) {
// This can be done in storyboard
standardLeftBtn.image = UIImage(systemName: "gear")
}


// Configure Refresh Control
tableView.refreshControl = self.refreshC
refreshC.addTarget(self, action: #selector(refresh(_:)), for: .valueChanged)


exportToggleBtn = UIBarButtonItem(title: "Select", style: .plain, target: self, action: #selector(toggleExportAll))
exportCommitBtn = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(doExport(_:)))
exportRightBtns = [exportCommitBtn, exportToggleBtn]
Expand Down Expand Up @@ -128,7 +135,7 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko

@objc private func refresh(_ sender: Any) {
list.reload()
refresher.endRefreshing()
self.refreshC.endRefreshing()
}

func preferredSystemOfUnitsChanged() {
Expand Down Expand Up @@ -225,12 +232,11 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko
tableView.deselectRow(at: indexPath, animated: true)
}

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// A second section is shown only iff additional workout can be loaded (or are being loaded)
if tableView.numberOfSections == 2, !list.isLoading, indexPath.section == 0 && indexPath.row == tableView.numberOfRows(inSection: 0) - 1 {
list.loadMore()
}
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == max(list.workouts?.count ?? 1, 1) - 1 && list.canDisplayMore && exporter == nil && !list.isLoading {
list.loadMore()
}
}

// MARK: - List Displaying

Expand Down Expand Up @@ -325,7 +331,7 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko
listChanged()

navigationItem.leftBarButtonItem = standardLeftBtn
navigationItem.rightBarButtonItem = standardRightBtn
navigationItem.rightBarButtonItems = standardRightBtns
}

private func updateExportToggleAll() {
Expand Down
Loading