From 5a23cce4bf2324b1e6e7021164383de15da84e1f Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 23 Dec 2024 16:50:45 -0800 Subject: [PATCH] feat: Improve SwiftUI and UIKit integration (#127) * feat: Improve SwiftUI and UIKit integration * Update project * Fix doxylamine graph in CareView * improve view localization * Task localization --- OCKSample.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../xcshareddata/xcschemes/OCKSample.xcscheme | 2 +- .../OCKWatchSample (Notification).xcscheme | 2 +- .../xcschemes/OCKWatchSample.xcscheme | 2 +- OCKSample/Extensions/OCKStore.swift | 128 +++++--- OCKSample/Main/Care/CareView.swift | 18 +- OCKSample/Main/Care/CareViewController.swift | 297 ++++++++++-------- OCKSample/Main/Login/LoginView.swift | 39 ++- OCKSample/Main/Profile/ProfileView.swift | 62 ++-- OCKSample/Models/TaskID.swift | 4 + .../Localization/en.lproj/Localizable.strings | 34 ++ .../Main/Care/CareView.swift | 30 +- .../Main/Login/LoginView.swift | 2 +- OCKWatchSample Extension/Main/MainView.swift | 8 +- 15 files changed, 396 insertions(+), 248 deletions(-) diff --git a/OCKSample.xcodeproj/project.pbxproj b/OCKSample.xcodeproj/project.pbxproj index cd943140..4571fe41 100644 --- a/OCKSample.xcodeproj/project.pbxproj +++ b/OCKSample.xcodeproj/project.pbxproj @@ -620,7 +620,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1200; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Network Reconnaissance Lab"; TargetAttributes = { 5173CB8623C3A846007655A0 = { @@ -1040,7 +1040,6 @@ 91AD924D24A4C42E00925D4D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1069,7 +1068,6 @@ 91AD924E24A4C42E00925D4D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1237,7 +1235,7 @@ repositoryURL = "https://github.com/netreconlab/CareKitEssentials"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "1.0.0-alpha.16"; + minimumVersion = "1.0.0-alpha.31"; }; }; 70202EBF2807333900CF73FB /* XCRemoteSwiftPackageReference "CareKit" */ = { @@ -1245,7 +1243,7 @@ repositoryURL = "https://github.com/cbaker6/CareKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "3.0.0-beta.14"; + minimumVersion = "3.0.0-beta.31"; }; }; 918FDEAD271B3F8F0045A0EF /* XCRemoteSwiftPackageReference "ParseCareKit" */ = { diff --git a/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2aad609d..3686c499 100644 --- a/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/cbaker6/CareKit.git", "state" : { - "revision" : "482e0b34b4116158178cd8f368aeedcb1c8c2b81", - "version" : "3.0.0-beta.15" + "revision" : "989454a4a5f0c8f23fd3344c6bb5aa8e3db185a7", + "version" : "3.0.0-beta.31" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/netreconlab/CareKitEssentials", "state" : { - "revision" : "02ea2bb43ae076f647c80d8045da7ea698749649", - "version" : "1.0.0-alpha.19" + "revision" : "8df6c52a65d7881611acb23178b39d08c44ebd21", + "version" : "1.0.0-alpha.32" } }, { diff --git a/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme index 50f6ab9a..54c47617 100644 --- a/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme +++ b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme @@ -1,6 +1,6 @@ some UIViewController { let viewController = createViewController() @@ -38,25 +37,20 @@ struct CareView: UIViewControllerRepresentable { } guard careViewController.store !== careStore || appDelegate?.isFirstTimeLogin == true else { - // No need to replace view - careViewController.events = events return } - navigationController.setViewControllers([createViewController()], animated: false) + let newCareViewController = createViewController() + navigationController.setViewControllers( + [newCareViewController], + animated: false + ) } func createViewController() -> UIViewController { CareViewController( - store: careStore, - events: events + store: careStore ) } - - static func query() -> OCKEventQuery { - var query = OCKEventQuery(for: Date()) - query.taskIDs = [TaskID.steps] - return query - } } struct CareView_Previews: PreviewProvider { diff --git a/OCKSample/Main/Care/CareViewController.swift b/OCKSample/Main/Care/CareViewController.swift index 31ffa290..1306923f 100644 --- a/OCKSample/Main/Care/CareViewController.swift +++ b/OCKSample/Main/Care/CareViewController.swift @@ -29,36 +29,25 @@ */ import CareKit +import CareKitEssentials import CareKitStore import CareKitUI import os.log import SwiftUI import UIKit +// swiftlint:disable type_body_length + +@MainActor class CareViewController: OCKDailyPageViewController { private var isSyncing = false private var isLoading = false - var events: CareStoreFetchedResults? { - didSet { - self.reloadView() - } + private var accentColor: Color { + Color(TintColorKey.defaultValue) } - - /// Create an instance of the view controller. Will hook up the calendar to the tasks collection, - /// and query and display the tasks. - /// - /// - Parameter store: The store from which to query the tasks. - /// - Parameter computeProgress: Used to compute the combined progress for a series of CareKit events. - init( - store: OCKAnyStoreProtocol, - events: CareStoreFetchedResults? = nil, - computeProgress: @escaping (OCKAnyEvent) -> CareTaskProgress = { event in - event.computeProgress(by: .checkingOutcomeExists) - } - ) { - super.init(store: store, computeProgress: computeProgress) - self.events = events + private var style: Styler { + CustomStylerKey.defaultValue } override func viewDidLoad() { @@ -115,7 +104,6 @@ class CareViewController: OCKDailyPageViewController { } } - @MainActor @objc private func synchronizeWithRemote() { guard !isSyncing else { return @@ -139,10 +127,7 @@ class CareViewController: OCKDailyPageViewController { guard !isLoading else { return } - DispatchQueue.main.async { - self.isLoading = true - self.reload() - } + self.reload() } /* @@ -154,145 +139,169 @@ class CareViewController: OCKDailyPageViewController { prepare listViewController: OCKListViewController, for date: Date ) { - Task { - do { - let tasks = try await fetchTasks(on: date) - let isCurrentDay = Calendar.current.isDate(date, inSameDayAs: Date()) - - let taskCards = tasks.compactMap { - let cards = self.taskViewController(for: $0, - on: date) - cards?.forEach { - if let carekitView = $0.view as? OCKView { - carekitView.customStyle = CustomStylerKey.defaultValue - } - $0.view.isUserInteractionEnabled = isCurrentDay - $0.view.alpha = !isCurrentDay ? 0.4 : 1.0 - } - return cards - } + self.isLoading = true + + // Always call this method to ensure dates for + // queries are correct. + let date = modifyDateIfNeeded(date) + let isCurrentDay = isSameDay(as: date) + + // Only show the tip view on the current date + if isCurrentDay { + if Calendar.current.isDate(date, inSameDayAs: Date()) { + // Add a non-CareKit view into the list + let tipTitle = "Benefits of exercising" + let tipText = "Learn how activity can promote a healthy pregnancy." + let tipView = TipView() + tipView.headerView.titleLabel.text = tipTitle + tipView.headerView.detailLabel.text = tipText + tipView.imageView.image = UIImage(named: "exercise.jpg") + tipView.customStyle = CustomStylerKey.defaultValue + listViewController.appendView(tipView, animated: false) + } + } - listViewController.clear() - - #if os(iOS) - // Only show the tip view on the current date - if isCurrentDay { - if Calendar.current.isDate(date, inSameDayAs: Date()) { - // Add a non-CareKit view into the list - let tipTitle = "Benefits of exercising" - let tipText = "Learn how activity can promote a healthy pregnancy." - let tipView = TipView() - tipView.headerView.titleLabel.text = tipTitle - tipView.headerView.detailLabel.text = tipText - tipView.imageView.image = UIImage(named: "exercise.jpg") - tipView.customStyle = CustomStylerKey.defaultValue - listViewController.appendView(tipView, animated: false) - } - } - #endif + fetchAndDisplayTasks(on: listViewController, for: date) + } - // Display the rest of the cards - taskCards.forEach { (cards: [UIViewController]) in - cards.forEach { - listViewController.appendViewController($0, animated: false) - } - } + private func isSameDay(as date: Date) -> Bool { + Calendar.current.isDate( + date, + inSameDayAs: Date() + ) + } - } catch { - Logger.feed.error("Could not fetch tasks: \(error)") - } - self.isLoading = false + private func modifyDateIfNeeded(_ date: Date) -> Date { + guard date < .now else { + return date } - + guard !isSameDay(as: date) else { + return .now + } + return date.endOfDay } - private func getStoreFetchRequestEvent(for taskId: String) -> CareStoreFetchedResult? { - events?.last(where: { $0.result.task.id == taskId }) + private func fetchAndDisplayTasks( + on listViewController: OCKListViewController, + for date: Date + ) { + Task { + let tasks = await self.fetchTasks(on: date) + appendTasks(tasks, to: listViewController, date: date) + } } - private func taskViewController( - for task: OCKAnyTask, + private func taskViewControllers( + _ task: OCKAnyTask, on date: Date ) -> [UIViewController]? { - var query = OCKEventQuery(for: Date()) + var query = OCKEventQuery(for: date) query.taskIDs = [task.id] switch task.id { case TaskID.steps: - guard let event = getStoreFetchRequestEvent(for: task.id) else { - return nil - } - let view = NumericProgressTaskView<_NumericProgressTaskViewHeader>( - event: event, - numberFormatter: .none + let card = EventQueryView( + query: query ) - .careKitStyle(CustomStylerKey.defaultValue) + .formattedHostingController() - return [view.formattedHostingController()] + return [card] case TaskID.stretch: - return [OCKInstructionsTaskViewController(query: query, - store: self.store)] + let card = EventQueryView( + query: query + ) + .formattedHostingController() + + return [card] case TaskID.kegels: /* Since the kegel task is only scheduled every other day, there will be cases where it is not contained in the tasks array returned from the query. */ - return [OCKSimpleTaskViewController(query: query, - store: self.store)] + let card = EventQueryView( + query: query + ) + .formattedHostingController() + + return [card] // Create a card for the doxylamine task if there are events for it on this day. case TaskID.doxylamine: - return [OCKChecklistTaskViewController(query: query, - store: self.store)] + // This is a UIKit based card. + let card = OCKChecklistTaskViewController( + query: query, + store: self.store + ) + + return [card] case TaskID.nausea: - var cards = [UIViewController]() + + let title = String(localized: "NAUSEA_DOXYLAMINE_INTAKE") + let subtitle = String(localized: "THIS_WEEK") + let duration = Calendar + .current + .dateIntervalOfWeek(for: Date()) + // dynamic gradient colors - let nauseaGradientStart = TintColorFlipKey.defaultValue - let nauseaGradientEnd = TintColorKey.defaultValue + let nauseaGradientStart = Color(TintColorFlipKey.defaultValue) + let nauseaGradientEnd = accentColor - // Create a plot comparing nausea to medication adherence. - let nauseaDataSeries = OCKDataSeriesConfiguration( + let nauseaDataSeries = CKEDataSeriesConfiguration( taskID: task.id, - legendTitle: "Nausea", + mark: .bar, + legendTitle: String(localized: "NAUSEA"), + color: nauseaGradientEnd, gradientStartColor: nauseaGradientStart, - gradientEndColor: nauseaGradientEnd, - markerSize: 10) { event in - event.computeProgress(by: .summingOutcomeValues) - } + stackingMethod: .unstacked + ) { event in + event.computeProgress(by: .summingOutcomeValues) + } - let doxylamineDataSeries = OCKDataSeriesConfiguration( - taskID: task.id, - legendTitle: "Doxylamine", - gradientStartColor: .systemGray2, - gradientEndColor: .systemGray, - markerSize: 10) { event in - event.computeProgress(by: .summingOutcomeValues) - } + let doxylamineDataSeries = CKEDataSeriesConfiguration( + taskID: TaskID.doxylamine, + mark: .bar, + legendTitle: String(localized: "DOXYLAMINE"), + color: Color(UIColor.systemGray2), + gradientStartColor: .gray, + stackingMethod: .unstacked + ) { event in + event.computeProgress(by: .summingOutcomeValues) + } - let insightsCard = OCKCartesianChartViewController( - plotType: .bar, - selectedDate: date, - configurations: [nauseaDataSeries, doxylamineDataSeries], - store: self.store) + let configurations = [ + nauseaDataSeries, + doxylamineDataSeries + ] - insightsCard.typedView.headerView.titleLabel.text = "Nausea & Doxylamine Intake" - insightsCard.typedView.headerView.detailLabel.text = "This Week" - insightsCard.typedView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week" - cards.append(insightsCard) + // This is a SwiftUI View Chart. + let chart = CareEssentialChartView( + title: title, + subtitle: subtitle, + dateInterval: duration, + period: .day, + configurations: configurations + ).formattedHostingController() /* - Also create a card that displays a single event. + Also create a card (UIKit view) that displays a single event. The event query passed into the initializer specifies that only today's log entries should be displayed by this log task view controller. */ - let nauseaCard = OCKButtonLogTaskViewController(query: query, - store: self.store) - cards.append(nauseaCard) + let nauseaCard = OCKButtonLogTaskViewController( + query: query, + store: self.store + ) + + let cards: [UIViewController] = [ + chart, + nauseaCard + ] + return cards default: @@ -300,18 +309,56 @@ class CareViewController: OCKDailyPageViewController { } } - private func fetchTasks(on date: Date) async throws -> [OCKAnyTask] { + private func appendTasks( + _ tasks: [OCKAnyTask], + to listViewController: OCKListViewController, + date: Date + ) { + let isCurrentDay = isSameDay(as: date) + tasks.compactMap { + let cards = self.taskViewControllers( + $0, + on: date + ) + cards?.forEach { + if let carekitView = $0.view as? OCKView { + carekitView.customStyle = style + } + $0.view.isUserInteractionEnabled = isCurrentDay + $0.view.alpha = !isCurrentDay ? 0.4 : 1.0 + } + return cards + }.forEach { (cards: [UIViewController]) in + cards.forEach { + let card = $0 + DispatchQueue.main.async { + listViewController.appendViewController(card, animated: true) + } + } + } + DispatchQueue.main.async { + self.isLoading = false + } + } + + private func fetchTasks(on date: Date) async -> [OCKAnyTask] { var query = OCKTaskQuery(for: date) query.excludesTasksWithNoEvents = true - let tasks = try await store.fetchAnyTasks(query: query) - let orderedTasks = TaskID.ordered.compactMap { orderedTaskID in - tasks.first(where: { $0.id == orderedTaskID }) + do { + let tasks = try await store.fetchAnyTasks(query: query) + let orderedTasks = TaskID.ordered.compactMap { orderedTaskID in + tasks.first(where: { $0.id == orderedTaskID }) + } + return orderedTasks + } catch { + Logger.feed.error("Could not fetch tasks: \(error, privacy: .public)") + return [] } - return orderedTasks } } private extension View { + /// Convert SwiftUI view to UIKit view. func formattedHostingController() -> UIHostingController { let viewController = UIHostingController(rootView: self) viewController.view.backgroundColor = .clear diff --git a/OCKSample/Main/Login/LoginView.swift b/OCKSample/Main/Login/LoginView.swift index a902955b..d28161b5 100644 --- a/OCKSample/Main/Login/LoginView.swift +++ b/OCKSample/Main/Login/LoginView.swift @@ -33,7 +33,7 @@ struct LoginView: View { var body: some View { VStack { // Change the title to the name of your application - Text("CareKit Sample App") + Text("APP_NAME") .font(.largeTitle) .foregroundColor(.white) .padding() @@ -51,9 +51,9 @@ struct LoginView: View { https://www.swiftkickmobile.com/creating-a-segmented-control-in-swiftui/ */ Picker(selection: $signupLoginSegmentValue, - label: Text("Login Picker")) { - Text("Login").tag(0) - Text("Sign Up").tag(1) + label: Text("LOGIN_PICKER")) { + Text("LOGIN").tag(0) + Text("SIGN_UP").tag(1) } .pickerStyle(.segmented) .background(Color(tintColorFlip)) @@ -61,12 +61,12 @@ struct LoginView: View { .padding() VStack(alignment: .leading) { - TextField("Username", text: $usersname) + TextField("USERNAME", text: $usersname) .padding() .background(.white) .cornerRadius(20.0) .shadow(radius: 10.0, x: 20, y: 10) - SecureField("Password", text: $password) + SecureField("PASSWORD", text: $password) .padding() .background(.white) .cornerRadius(20.0) @@ -74,13 +74,13 @@ struct LoginView: View { switch signupLoginSegmentValue { case 1: - TextField("First Name", text: $firstName) + TextField("GIVEN_NAME", text: $firstName) .padding() .background(.white) .cornerRadius(20.0) .shadow(radius: 10.0, x: 20, y: 10) - TextField("Last Name", text: $lastName) + TextField("FAMILY_NAME", text: $lastName) .padding() .background(.white) .cornerRadius(20.0) @@ -114,13 +114,13 @@ struct LoginView: View { }, label: { switch signupLoginSegmentValue { case 1: - Text("Sign Up") + Text("SIGN_UP") .font(.headline) .foregroundColor(.white) .padding() .frame(width: 300) default: - Text("Login") + Text("LOGIN") .font(.headline) .foregroundColor(.white) .padding() @@ -137,7 +137,7 @@ struct LoginView: View { }, label: { switch signupLoginSegmentValue { case 0: - Text("Login Anonymously") + Text("LOGIN_ANONYMOUSLY") .font(.headline) .foregroundColor(.white) .padding() @@ -151,15 +151,22 @@ struct LoginView: View { // If an error occurs show it on the screen if let error = viewModel.loginError { - Text("Error: \(error.message)") + Text("\(String(localized: "ERROR")): \(error.message)") .foregroundColor(.red) } Spacer() } - .background(LinearGradient(gradient: Gradient(colors: [Color(tintColorFlip), - Color(tintColor)]), - startPoint: .top, - endPoint: .bottom)) + .background( + LinearGradient( + gradient: Gradient( + colors: [ + Color(tintColorFlip), + Color(tintColor)] + ), + startPoint: .top, + endPoint: .bottom + ) + ) } } diff --git a/OCKSample/Main/Profile/ProfileView.swift b/OCKSample/Main/Profile/ProfileView.swift index eadcb8ae..10ef5f98 100644 --- a/OCKSample/Main/Profile/ProfileView.swift +++ b/OCKSample/Main/Profile/ProfileView.swift @@ -21,24 +21,30 @@ struct ProfileView: View { var body: some View { VStack { VStack(alignment: .leading) { - TextField("First Name", - text: $viewModel.firstName) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) + TextField( + "GIVEN_NAME", + text: $viewModel.firstName + ) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) - TextField("Last Name", - text: $viewModel.lastName) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) + TextField( + "FAMILY_NAME", + text: $viewModel.lastName + ) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) - DatePicker("Birthday", - selection: $viewModel.birthday, - displayedComponents: [DatePickerComponents.date]) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) + DatePicker( + "BIRTHDAY", + selection: $viewModel.birthday, + displayedComponents: [DatePickerComponents.date] + ) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) } Button(action: { @@ -50,11 +56,13 @@ struct ProfileView: View { } } }, label: { - Text("Save Profile") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(width: 300, height: 50) + Text( + "SAVE_PROFILE" + ) + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) }) .background(Color(.green)) .cornerRadius(15) @@ -66,11 +74,13 @@ struct ProfileView: View { await loginViewModel.logout() } }, label: { - Text("Log Out") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(width: 300, height: 50) + Text( + "LOG_OUT" + ) + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) }) .background(Color(.red)) .cornerRadius(15) diff --git a/OCKSample/Models/TaskID.swift b/OCKSample/Models/TaskID.swift index 192ff2aa..86b40fb5 100644 --- a/OCKSample/Models/TaskID.swift +++ b/OCKSample/Models/TaskID.swift @@ -18,4 +18,8 @@ enum TaskID { static var ordered: [String] { [Self.steps, Self.doxylamine, Self.kegels, Self.stretch, Self.nausea] } + + static var orderedObjective: [String] { + [Self.doxylamine, Self.kegels, Self.stretch] + } } diff --git a/OCKSample/Supporting Files/Localization/en.lproj/Localizable.strings b/OCKSample/Supporting Files/Localization/en.lproj/Localizable.strings index 9af0e415..bc01d63d 100644 --- a/OCKSample/Supporting Files/Localization/en.lproj/Localizable.strings +++ b/OCKSample/Supporting Files/Localization/en.lproj/Localizable.strings @@ -62,3 +62,37 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. "THREE_FINGER_SWIPE_WEEK" = "Three-finger swipe to go to next or previous week"; "TODAY" = "Today"; "START_SURVEY" = "Start Survey"; + +// MARK: Login +"APP_NAME" = "CareKit Sample App"; +"LOGIN_PICKER" = "Login Picker"; +"LOGIN" = "Login"; +"LOGIN_ANONYMOUSLY" = "Login Anonymously"; +"SIGN_UP" = "Sign Up"; +"USERNAME" = "Username"; +"PASSWORD" = "Password"; +"GIVEN_NAME" = "First Name"; +"FAMILY_NAME" = "Last Name"; +"ERROR" = "Error"; +"OPEN_APP_IPHONE" = "Please open the app on your iPhone"; + +// MARK: Care View +"NAUSEA_DOXYLAMINE_INTAKE" = "Nausea & Doxylamine Intake"; +"THIS_WEEK" = "This Week"; +"NAUSEA" = "Nausea"; +"DOXYLAMINE" = "Doxylamine"; + +// MARK: Profile +"BIRTHDAY" = "Birthday"; +"SAVE_PROFILE" = "Save Profile"; +"LOG_OUT" = "Log Out"; + +// MARK: TASKS +"TAKE DOXYLAMINE" = "Take Doxylamine"; +"DOXYLAMINE_INSTRUCTIONS" = "Take 25mg of doxylamine when you experience nausea"; +"ANYTIME_DURING_DAY" = "Anytime throughout the day"; +"TRACK NAUSEA" = "Track your nausea"; +"NAUSEA_INSTRUCTIONS" = "Tap the button below anytime you experience nausea"; +"KEGEL_EXERCISES" = "Kegel Exercises"; +"KEGEL_INSTRUCTIONS" = "Perform kegel exercies"; +"STRETCH" = "Stretch"; diff --git a/OCKWatchSample Extension/Main/Care/CareView.swift b/OCKWatchSample Extension/Main/Care/CareView.swift index 26de90c6..12137da9 100644 --- a/OCKWatchSample Extension/Main/Care/CareView.swift +++ b/OCKWatchSample Extension/Main/Care/CareView.swift @@ -7,6 +7,7 @@ // import CareKit +import CareKitEssentials import CareKitStore import CareKitUI import SwiftUI @@ -15,24 +16,47 @@ import os.log struct CareView: View { @CareStoreFetchRequest(query: query()) private var events + @State var sortedTaskIDs: [String: Int] = [:] + + private var orderedEvents: [CareStoreFetchedResult] { + events.latest.sorted(by: { left, right in + let leftTaskID = left.result.task.id + let rightTaskID = right.result.task.id + + return sortedTaskIDs[leftTaskID] ?? 0 < sortedTaskIDs[rightTaskID] ?? 0 + }) + } var body: some View { ScrollView { - ForEach(events) { event in + ForEach(orderedEvents) { event in if event.result.task.id == TaskID.kegels { SimpleTaskView(event: event) - } else if event.result.task.id == TaskID.stretch { + } else { InstructionsTaskView(event: event) } } + }.onAppear { + let taskIDs = TaskID.orderedObjective + sortedTaskIDs = computeTaskIDOrder(taskIDs: taskIDs) + events.query.taskIDs = taskIDs } } static func query() -> OCKEventQuery { var query = OCKEventQuery(for: Date()) - query.taskIDs = [TaskID.stretch, TaskID.kegels] + query.taskIDs = TaskID.orderedObjective return query } + + private func computeTaskIDOrder(taskIDs: [String]) -> [String: Int] { + // Tie index values to TaskIDs. + let sortedTaskIDs = taskIDs.enumerated().reduce(into: [String: Int]()) { taskDictionary, task in + taskDictionary[task.element] = task.offset + } + + return sortedTaskIDs + } } struct ContentView_Previews: PreviewProvider { diff --git a/OCKWatchSample Extension/Main/Login/LoginView.swift b/OCKWatchSample Extension/Main/Login/LoginView.swift index 75d1a9bd..bd00bee6 100644 --- a/OCKWatchSample Extension/Main/Login/LoginView.swift +++ b/OCKWatchSample Extension/Main/Login/LoginView.swift @@ -12,7 +12,7 @@ struct LoginView: View { @ObservedObject var viewModel: LoginViewModel var body: some View { - Text("Please open the OCKSample app on your iPhone") + Text("OPEN_APP_IPHONE") .multilineTextAlignment(.center) .padding() Image(systemName: "apps.iphone") diff --git a/OCKWatchSample Extension/Main/MainView.swift b/OCKWatchSample Extension/Main/MainView.swift index 7e5e0311..e81d4c87 100644 --- a/OCKWatchSample Extension/Main/MainView.swift +++ b/OCKWatchSample Extension/Main/MainView.swift @@ -14,8 +14,10 @@ struct MainView: View { @EnvironmentObject private var appDelegate: AppDelegate @StateObject private var loginViewModel = LoginViewModel() @State private var path = [MainViewPath]() - @State private var store = OCKStore(name: Constants.noCareStoreName, - type: .inMemory) + @State private var store = OCKStore( + name: Constants.noCareStoreName, + type: .inMemory + ) var body: some View { NavigationStack(path: $path) { @@ -57,7 +59,7 @@ struct MainView: View { await loginViewModel.checkStatus() } guard let newStore = newStore, - store.name != newStore.name else { + store !== newStore else { return } store = newStore