diff --git a/Workout Core/Model/Preferences.swift b/Workout Core/Model/Preferences.swift index 52a3522..a9a6b61 100644 --- a/Workout Core/Model/Preferences.swift +++ b/Workout Core/Model/Preferences.swift @@ -16,6 +16,7 @@ private enum PreferenceKeys: String, KeyValueStoreKey { case maxHeartRate = "maxHeartRate" case runningHeartZones = "runningHeartZones" case exportRouteType = "exportRouteType" + case defaultCalendar = "defaultCalendar" case reviewRequestCounter = "reviewRequestCounter" @@ -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() @@ -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) @@ -74,6 +76,8 @@ public class Preferences { d.reviewCounterUpdated?() case .routeType: d.routeTypeChanged?() + case .defaultCalendar: + d.defaultCalendarChanged?() case .generic: d.preferencesChanged?() } @@ -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) + } + } + } diff --git a/Workout Core/Model/Workout/Workout.swift b/Workout Core/Model/Workout/Workout.swift index d5f7ff8..9289098 100644 --- a/Workout Core/Model/Workout/Workout.swift +++ b/Workout Core/Model/Workout/Workout.swift @@ -8,6 +8,7 @@ import HealthKit import MBLibrary +import EventKit public protocol WorkoutDelegate: AnyObject { @@ -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 + } + + 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) + 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 + 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)")!) + } + } } diff --git a/Workout.xcodeproj/project.pbxproj b/Workout.xcodeproj/project.pbxproj index 90cbd10..38af85f 100644 --- a/Workout.xcodeproj/project.pbxproj +++ b/Workout.xcodeproj/project.pbxproj @@ -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 */; }; @@ -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 = ""; }; 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 = ""; }; + 3B598F2923BD1BA70046A5FC /* CalendarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTableViewCell.swift; sourceTree = ""; }; 3BD3437423A998CC00567BD7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 3BD3437623A998CC00567BD7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 3BD3437823A998D800567BD7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; @@ -327,6 +331,7 @@ 7A650B5322C1354300059687 /* LoadMoreCell.swift */, 7A9BF6D322CA317600EC1D95 /* FilterCells.swift */, 7AE6A94122E793E600D2ED88 /* WorkoutGeneralDataCell.swift */, + 3B598F2923BD1BA70046A5FC /* CalendarTableViewCell.swift */, ); path = View; sourceTree = ""; @@ -339,6 +344,7 @@ 7A650B4522C1354000059687 /* StepSourceTVC.swift */, 7A650B4422C1354000059687 /* RunningHeartZonesTVC.swift */, 7A1F8CC123AF5610008EA2E9 /* RouteTypeTVC.swift */, + 3B598F2123BD1B7D0046A5FC /* SelectCalendarTVC.swift */, ); path = Settings; sourceTree = ""; @@ -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 */, ); diff --git a/Workout/Base.lproj/Localizable.strings b/Workout/Base.lproj/Localizable.strings index a31d077..73c8913 100644 --- a/Workout/Base.lproj/Localizable.strings +++ b/Workout/Base.lproj/Localizable.strings @@ -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" = ": "; diff --git a/Workout/Controller/ListTVC.swift b/Workout/Controller/ListTVC.swift index be59e6b..77f5466 100644 --- a/Workout/Controller/ListTVC.swift +++ b/Workout/Controller/ListTVC.swift @@ -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! @@ -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] @@ -128,7 +135,7 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko @objc private func refresh(_ sender: Any) { list.reload() - refresher.endRefreshing() + self.refreshC.endRefreshing() } func preferredSystemOfUnitsChanged() { @@ -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 @@ -325,7 +331,7 @@ class ListTableViewController: UITableViewController, WorkoutListDelegate, Worko listChanged() navigationItem.leftBarButtonItem = standardLeftBtn - navigationItem.rightBarButtonItem = standardRightBtn + navigationItem.rightBarButtonItems = standardRightBtns } private func updateExportToggleAll() { diff --git a/Workout/Controller/Settings/AboutVC.swift b/Workout/Controller/Settings/AboutVC.swift index 90c4e9e..bdb7d4f 100644 --- a/Workout/Controller/Settings/AboutVC.swift +++ b/Workout/Controller/Settings/AboutVC.swift @@ -10,6 +10,7 @@ import HealthKit import UIKit import MBLibrary import WorkoutCore +import EventKit class AboutViewController: UITableViewController, PreferencesDelegate, RemoveAdsDelegate { @@ -62,7 +63,7 @@ class AboutViewController: UITableViewController, PreferencesDelegate, RemoveAds switch section { // Settings case settingsSectionOffset: - return 4 + return 5 // Source Code & Contacts case settingsSectionOffset + 1: return 2 @@ -96,6 +97,11 @@ class AboutViewController: UITableViewController, PreferencesDelegate, RemoveAds let cell = tableView.dequeueReusableCell(withIdentifier: "routeType", for: indexPath) setRouteType(in: cell) return cell + // Default Calendar + case (settingsSectionOffset, 4): + let cell = tableView.dequeueReusableCell(withIdentifier: "defaultCalendar", for: indexPath) + setDefaultCalendar(in: cell) + return cell // Source Code case (settingsSectionOffset + 1, 0): return tableView.dequeueReusableCell(withIdentifier: "sourceCode", for: indexPath) @@ -164,6 +170,12 @@ class AboutViewController: UITableViewController, PreferencesDelegate, RemoveAds } } + func defaultCalendarChanged() { + if let cell = tableView.cellForRow(at: IndexPath(row: 4, section: settingsSectionOffset)) { + setDefaultCalendar(in: cell) + } + } + private func setUnits(in cell: UITableViewCell) { cell.detailTextLabel?.text = preferences.systemOfUnits.displayName } @@ -187,6 +199,13 @@ class AboutViewController: UITableViewController, PreferencesDelegate, RemoveAds cell.detailTextLabel?.text = preferences.routeType.displayName } + private func setDefaultCalendar(in cell: UITableViewCell) { + if preferences.defaultCalendarSelected == "" && EKEventStore().defaultCalendarForNewEvents != nil { + preferences.defaultCalendarSelected = EKEventStore().defaultCalendarForNewEvents?.calendarIdentifier ?? "" + } + cell.detailTextLabel?.text = EKEventStore().calendar(withIdentifier: preferences.defaultCalendarSelected)?.title + } + // MARK: - Ads management @IBAction func removeAds() { diff --git a/Workout/Controller/Settings/SelectCalendarTVC.swift b/Workout/Controller/Settings/SelectCalendarTVC.swift new file mode 100644 index 0000000..f5c94fa --- /dev/null +++ b/Workout/Controller/Settings/SelectCalendarTVC.swift @@ -0,0 +1,145 @@ +// +// SelectCalendarTVC.swift +// Workout +// +// Created by Maxime Killinger on 16/12/2019. +// Copyright © 2019 Marco Boschi. All rights reserved. +// + +import UIKit +import EventKit + +class SelectCalendarTableViewController: UITableViewController { + + var calendars: [EKCalendar]? + let refreshC = UIRefreshControl() + + override func viewDidLoad() { + super.viewDidLoad() + checkCalendarAuthorizationStatus() + + // Configure Refresh Control + self.tableView.refreshControl = self.refreshC + refreshC.addTarget(self, action: #selector(calendarDidAdd(_:)), for: .valueChanged) + } + + override func viewWillAppear(_ animated: Bool) { + checkCalendarAuthorizationStatus() + } + + func checkCalendarAuthorizationStatus() { + let status = EKEventStore.authorizationStatus(for: EKEntityType.event) + + switch (status) { + case EKAuthorizationStatus.notDetermined: + requestAccessToCalendar() + case EKAuthorizationStatus.authorized: + loadCalendars() + refreshTableView() + case EKAuthorizationStatus.restricted, EKAuthorizationStatus.denied: + loadCalendars() + refreshTableView() + @unknown default: + loadCalendars() + refreshTableView() + } + } + + func requestAccessToCalendar() { + EKEventStore().requestAccess(to: .event, completion: { + (accessGranted: Bool, error: Error?) in + + if accessGranted == true { + DispatchQueue.main.async(execute: { + self.loadCalendars() + self.refreshTableView() + }) + } else { + DispatchQueue.main.async(execute: { + return + }) + } + }) + } + + func loadCalendars() { + self.calendars = EKEventStore().calendars(for: EKEntityType.event) + calendars?.forEach { + if !$0.allowsContentModifications { + calendars?.removeElement($0) + } + } + self.calendars = self.calendars?.sorted() { (cal1, cal2) -> Bool in + return cal1.title < cal2.title + } + } + + func refreshTableView() { + self.tableView.reloadData() + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let calendars = self.calendars { + return calendars.count == 0 ? 1 : calendars.count + } + + return 0 + } + + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell")! as! CalendarTableViewCell + + if calendars != nil && !(calendars?.isEmpty ?? true), let calendars = self.calendars { + let calendarName = calendars[(indexPath as NSIndexPath).row].title + let calendarIdentifier = calendars[(indexPath as NSIndexPath).row].calendarIdentifier + + cell.calendarName?.text = calendarName.htmlAttributedString?.string + cell.calendarUniqueIdentifier = calendarIdentifier + if preferences.defaultCalendarSelected == "" && calendarIdentifier == EKEventStore().defaultCalendarForNewEvents?.calendarIdentifier { + cell.accessoryType = .checkmark + preferences.defaultCalendarSelected = EKEventStore().defaultCalendarForNewEvents?.calendarIdentifier ?? "" + } + if calendarIdentifier == preferences.defaultCalendarSelected { + cell.accessoryType = .checkmark + } + cell.calendarColoredDot.backgroundColor = UIColor(cgColor: calendars[(indexPath as NSIndexPath).row].cgColor) + } else { + cell.textLabel?.text = "Unknown Calendar Name" + cell.calendarUniqueIdentifier = nil + cell.isUserInteractionEnabled = false + } + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath) as? CalendarTableViewCell { + if cell.accessoryType == .checkmark { + cell.accessoryType = .none + if EKEventStore().defaultCalendarForNewEvents != nil { + preferences.defaultCalendarSelected = EKEventStore().defaultCalendarForNewEvents?.calendarIdentifier ?? "" + for i in 0..<(tableView.numberOfRows(inSection: 0)) { + if ((tableView.cellForRow(at: IndexPath(row: i, section: 0)) as! CalendarTableViewCell).calendarUniqueIdentifier == preferences.defaultCalendarSelected) { + tableView.cellForRow(at: IndexPath(row: i, section: 0))?.accessoryType = .checkmark + } + } + } + } else { + for i in 0..<(tableView.numberOfRows(inSection: 0)) { + tableView.cellForRow(at: IndexPath(row: i, section: 0))?.accessoryType = .none + } + cell.accessoryType = .checkmark + preferences.defaultCalendarSelected = cell.calendarUniqueIdentifier ?? "" + } + tableView.deselectRow(at: indexPath, animated: true) + } + } + + // MARK: Calendar Added Delegate + @objc func calendarDidAdd(_ sender: Any) { + self.loadCalendars() + self.refreshTableView() + self.refreshC.endRefreshing() + } +} diff --git a/Workout/Controller/WorkoutTVC.swift b/Workout/Controller/WorkoutTVC.swift index 1f536cf..cf16f85 100644 --- a/Workout/Controller/WorkoutTVC.swift +++ b/Workout/Controller/WorkoutTVC.swift @@ -16,7 +16,8 @@ class WorkoutTableViewController: UITableViewController, WorkoutDelegate { private static let defaultHeight: CGFloat = 44 @IBOutlet var exportBtn: UIBarButtonItem! - + @IBOutlet var icsBtn: UIBarButtonItem! + weak var listController: ListTableViewController! var rawWorkout: HKWorkout! private var workout: Workout! @@ -56,7 +57,8 @@ class WorkoutTableViewController: UITableViewController, WorkoutDelegate { func workoutLoaded(_ workout: Workout) { DispatchQueue.main.async { self.exportBtn.isEnabled = !self.workout.hasError - self.navigationItem.setRightBarButton(self.exportBtn, animated: true) + self.icsBtn.isEnabled = self.exportBtn.isEnabled + self.navigationItem.rightBarButtonItems = [self.exportBtn, self.icsBtn] let old = self.tableView.numberOfSections let new = self.numberOfSections(in: self.tableView) @@ -373,4 +375,40 @@ class WorkoutTableViewController: UITableViewController, WorkoutDelegate { } } + // MARK: - ICS Export + + @IBAction func ICSexport(_ sender: UIBarButtonItem) { + loadingIndicator?.dismiss(animated: false) + loadingIndicator = UIAlertController.getModalLoading() + self.present(loadingIndicator!, animated: true) + + workout.ICSexport(for: preferences.systemOfUnits) { result in + guard let calendar = result else { + let alert = UIAlertController(simpleAlert: NSLocalizedString("EXPORT_ERROR", comment: "Export error"), message: nil) + + DispatchQueue.main.async { + if let l = self.loadingIndicator { + l.dismiss(animated: true) { + self.loadingIndicator = nil + self.present(alert, animated: true) + } + } else { + self.present(alert, animated: true) + } + } + + return + } + + DispatchQueue.main.async { + if let l = self.loadingIndicator { + l.dismiss(animated: true) { + self.loadingIndicator = nil + } + UIApplication.shared.open(calendar) + } + } + } + } + } diff --git a/Workout/Support/Extensions.swift b/Workout/Support/Extensions.swift index 6607cc7..d8d374f 100644 --- a/Workout/Support/Extensions.swift +++ b/Workout/Support/Extensions.swift @@ -9,6 +9,7 @@ import Foundation import MBLibrary import WorkoutCore +import UIKit extension WorkoutList { @@ -38,3 +39,24 @@ extension WorkoutList { } } + +extension UIView { + func fadeIn(_ duration: TimeInterval = 1.0, delay: TimeInterval = 0.0, completion: @escaping ((Bool) -> Void) = {(finished: Bool) -> Void in}) { + UIView.animate(withDuration: duration, delay: delay, options: UIView.AnimationOptions.curveEaseIn, animations: { + self.alpha = 1.0 + }, completion: completion) } + + func fadeOut(_ duration: TimeInterval = 1.0, delay: TimeInterval = 0.0, completion: @escaping (Bool) -> Void = {(finished: Bool) -> Void in}) { + UIView.animate(withDuration: duration, delay: delay, options: UIView.AnimationOptions.curveEaseIn, animations: { + self.alpha = 0.0 + }, completion: completion) + } +} + +extension String { + /// Converts HTML string to a `NSAttributedString` + + var htmlAttributedString: NSAttributedString? { + return try? NSAttributedString(data: Data(utf8), options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) + } +} diff --git a/Workout/View/Base.lproj/Main.storyboard b/Workout/View/Base.lproj/Main.storyboard index 04ae8fd..0eb713b 100644 --- a/Workout/View/Base.lproj/Main.storyboard +++ b/Workout/View/Base.lproj/Main.storyboard @@ -296,14 +296,22 @@ - - - - - + + + + + + + + + + + + + @@ -675,9 +683,37 @@ - + + + + + + + + + + + + + + + + + @@ -694,7 +730,7 @@ - + @@ -1115,6 +1151,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1125,6 +1217,7 @@ + diff --git a/Workout/View/CalendarTableViewCell.swift b/Workout/View/CalendarTableViewCell.swift new file mode 100644 index 0000000..e43c578 --- /dev/null +++ b/Workout/View/CalendarTableViewCell.swift @@ -0,0 +1,22 @@ +// +// CalendarTableViewCell.swift +// Workout +// +// Created by Maxime Killinger on 18/12/2019. +// Copyright © 2019 Marco Boschi. All rights reserved. +// + +import UIKit + +class CalendarTableViewCell: UITableViewCell { + @IBOutlet weak var calendarColoredDot: UIView! + @IBOutlet weak var calendarName: UILabel! + + var calendarUniqueIdentifier : String? + + override func layoutSubviews() { + super.layoutSubviews() + calendarColoredDot.layer.cornerRadius = calendarColoredDot.frame.size.width / 2 + calendarColoredDot.clipsToBounds = true + } +}