From 7f27446b949a430839c6e406f50b9ebb86aeceaf Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 17 Feb 2026 11:42:16 -0600 Subject: [PATCH] Fix 8 audit items: remove force-unwraps, improve accessibility and concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace force-unwrap HK types with modern HKQuantityType(_:) initializer - Replace Calendar.date force-unwraps with guard/let in HealthService, HeaderPercView, MoodStreakActivity, DayViewViewModel, MonthTotalTemplate - Extract DayViewViewModel.countEntries into testable static method with safe flatMap - Replace DispatchQueue.main.asyncAfter with Task.sleep in CelebrationAnimations - Add .minimumScaleFactor(0.5) to SmallRollUpHeaderView for Dynamic Type - Add VoiceOver accessibility labels to HeaderPercView mood percentages - Fix @testable import iFeel → Feels in Tests_iOS.swift - Add 4 unit tests for countEntries (TDD) Co-Authored-By: Claude Opus 4.6 --- Feels.xcodeproj/project.pbxproj | 4 + Feels/Localizable.xcstrings | 114 ++++++++++++------ FeelsTests/DayViewViewModelTests.swift | 42 +++++++ Shared/MoodStreakActivity.swift | 5 +- Shared/Services/HealthService.swift | 28 +++-- Shared/Views/CelebrationAnimations.swift | 12 +- Shared/Views/DayView/DayViewViewModel.swift | 19 ++- Shared/Views/HeaderPercView.swift | 12 +- .../SharingTemplates/MonthTotalTemplate.swift | 2 +- Shared/Views/SmallRollUpHeaderView.swift | 1 + Tests iOS/Tests_iOS.swift | 5 +- 11 files changed, 168 insertions(+), 76 deletions(-) create mode 100644 FeelsTests/DayViewViewModelTests.swift diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index efdfaa1..7c2fc61 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 06E4767B5977FAC8B644FC92 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */; }; + A1B2C3D4E5F607080910ABCD /* DayViewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */; }; 1C0DAB51279DB0FB003B1F21 /* Feels/Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB50279DB0FB003B1F21 /* Feels/Localizable.xcstrings */; }; 1C0DAB52279DB0FB003B1F22 /* Feels/Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB50279DB0FB003B1F21 /* Feels/Localizable.xcstrings */; }; 1C9566442EF8F5F70032E68F /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 1C9566432EF8F5F70032E68F /* Algorithms */; }; @@ -135,6 +136,7 @@ 29E2A2FC314F88244CA946BF /* StreakTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StreakTests.swift; sourceTree = ""; }; 5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataControllerCRUDTests.swift; sourceTree = ""; }; 9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; + D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DayViewViewModelTests.swift; sourceTree = ""; }; B60015D02A064FF582E232FD /* Feels Watch App/Feels Watch AppDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch AppDebug.entitlements"; sourceTree = ""; }; B8AB4CD73C2B4DC89C6FE84D /* Feels Watch App/Feels Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch App.entitlements"; sourceTree = ""; }; DA0D74ACDD741CFA1F14F50F /* FeelsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeelsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -418,6 +420,7 @@ isa = PBXGroup; children = ( 5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */, + D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */, 9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */, 29E2A2FC314F88244CA946BF /* StreakTests.swift */, DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */, @@ -784,6 +787,7 @@ buildActionMask = 2147483647; files = ( EEB21B1CAA8EAEB497BD9FB3 /* DataControllerCRUDTests.swift in Sources */, + A1B2C3D4E5F607080910ABCD /* DayViewViewModelTests.swift in Sources */, 06E4767B5977FAC8B644FC92 /* IntegrationTests.swift in Sources */, 54259F7B3F4E959B3F4055E4 /* StreakTests.swift in Sources */, 9559409B5AEEAB40EBCB6AF9 /* VoteLogicsTests.swift in Sources */, diff --git a/Feels/Localizable.xcstrings b/Feels/Localizable.xcstrings index e63d029..a251c17 100644 --- a/Feels/Localizable.xcstrings +++ b/Feels/Localizable.xcstrings @@ -952,6 +952,9 @@ } } } + }, + "%lld day streak" : { + }, "%lld days" : { "comment" : "A secondary label below the year, showing the total number of days in that year. The argument is the total number of days in the year.", @@ -2198,6 +2201,18 @@ } } }, + "AI insights in light & dark mode" : { + "comment" : "A description of the feature that allows users to export insights from the app.", + "isCommentAutoGenerated" : true + }, + "All sizes & theme variations" : { + "comment" : "A description of what the \"Export Voting Layouts\" button does.", + "isCommentAutoGenerated" : true + }, + "All styles & complications" : { + "comment" : "A description of the feature that allows users to export all watch view screenshots.", + "isCommentAutoGenerated" : true + }, "All Time Moods" : { }, @@ -2328,7 +2343,6 @@ } }, "Animation Lab" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2704,6 +2718,10 @@ } } }, + "Bypass Subscription" : { + "comment" : "A label displayed next to a toggle switch in the \"Debug\" section.", + "isCommentAutoGenerated" : true + }, "Cancel" : { "comment" : "The text for a button that dismisses the current view.", "isCommentAutoGenerated" : true, @@ -3168,7 +3186,6 @@ }, "Clear All Data" : { "comment" : "A button label that clears all user data.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -4312,7 +4329,6 @@ }, "Current Parameters" : { "comment" : "A section header that lists various current parameters related to the app.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -4355,7 +4371,6 @@ }, "Current Streak" : { "comment" : "A label describing the user's current streak of using the app.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -4892,7 +4907,6 @@ }, "Days Using App" : { "comment" : "A label describing the number of days the app has been used.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -4935,7 +4949,6 @@ }, "Debug" : { "comment" : "A section header in the settings view, hidden in release builds.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -5579,7 +5592,6 @@ }, "Delete all mood entries" : { "comment" : "A description of what happens when the \"Clear All Data\" button is tapped.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -5664,7 +5676,6 @@ }, "Delete HealthKit Data" : { "comment" : "A button label that deletes all State of Mind records from HealthKit.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -6172,7 +6183,6 @@ } }, "Elevate Your\nEmotional Life" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -6552,7 +6562,6 @@ } }, "Experiment with vote celebrations" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -6806,6 +6815,10 @@ } } }, + "Export Insights Screenshots" : { + "comment" : "A button label that allows users to export insights screenshots.", + "isCommentAutoGenerated" : true + }, "Export Preview" : { "comment" : "A title for the preview section of the export view.", "isCommentAutoGenerated" : true, @@ -6848,9 +6861,16 @@ } } }, + "Export Voting Layouts" : { + "comment" : "A button label that allows users to export all of their voting layout configurations.", + "isCommentAutoGenerated" : true + }, + "Export Watch Screenshots" : { + "comment" : "A button label that allows users to export watch view screenshots.", + "isCommentAutoGenerated" : true + }, "Export Widget Screenshots" : { "comment" : "A button label that prompts the user to download their light and dark mode widget screenshots.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -7064,7 +7084,6 @@ } }, "Feel With\nAll Your Heart" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -7272,6 +7291,10 @@ } } }, + "Fill 2 years data + export PNGs" : { + "comment" : "A description of the feature that generates and exports sharing screenshots.", + "isCommentAutoGenerated" : true + }, "Find Your\nInner Calm" : { "comment" : "A title describing the main benefit of the premium subscription.", "isCommentAutoGenerated" : true, @@ -7315,7 +7338,6 @@ } }, "Find Your\nInner Peace" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -7479,6 +7501,10 @@ } } }, + "Generate & Export Sharing" : { + "comment" : "A button label that allows users to generate and export all sharing screenshots.", + "isCommentAutoGenerated" : true + }, "Get Mood Streak" : { "comment" : "Title of an intent that checks the user's current mood logging streak.", "isCommentAutoGenerated" : true, @@ -7691,7 +7717,6 @@ }, "Green dot = eligible to show. Tips only show once per session when eligible." : { "comment" : "A footer label explaining that tips are only shown once per session and that the green dot indicates whether a tip is currently eligible to be shown.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -7734,7 +7759,6 @@ }, "Has Seen Settings" : { "comment" : "A label for whether the user has seen the settings section in the app.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -7779,8 +7803,11 @@ "comment" : "A description of the analytics toggle.", "isCommentAutoGenerated" : true }, + "Hide trial banner & grant full access" : { + "comment" : "A description of the feature that allows users to bypass the trial period and access all features without ads.", + "isCommentAutoGenerated" : true + }, "How are you feeling?" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -7819,6 +7846,9 @@ } } } + }, + "How do you feel?" : { + }, "How to add widgets" : { "localizations" : { @@ -8421,7 +8451,6 @@ }, "Light & dark mode PNGs" : { "comment" : "A description of what the \"Export Widget Screenshots\" button does.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -8507,6 +8536,12 @@ "Live Activity Preview" : { "comment" : "The title of the view.", "isCommentAutoGenerated" : true + }, + "Log" : { + + }, + "Log mood" : { + }, "Log Mood" : { "comment" : "A button that opens Feels to log a mood.", @@ -8671,6 +8706,9 @@ } } } + }, + "Log your mood" : { + }, "Logged!" : { "comment" : "A message displayed when a mood is successfully logged.", @@ -8721,7 +8759,6 @@ }, "Make Tracking\nFun Again!" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -9015,7 +9052,6 @@ }, "Mood Log Count" : { "comment" : "The title of a label displaying the count of mood logs.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -10988,7 +11024,6 @@ "isCommentAutoGenerated" : true }, "Paywall Styles" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -11029,7 +11064,6 @@ } }, "Paywall Theme Lab" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -11572,7 +11606,6 @@ } }, "Preview" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -11613,7 +11646,6 @@ } }, "Preview and test different subscription paywall designs" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -11655,7 +11687,6 @@ }, "Preview subscription themes" : { "comment" : "A description of what the paywall preview button does.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -12508,7 +12539,6 @@ }, "Remove all State of Mind records" : { "comment" : "A description of what happens when the \"Delete HealthKit Data\" button is pressed.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -12681,7 +12711,6 @@ }, "Reset All Tips" : { "comment" : "A button that resets all tips to their default state.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -13323,9 +13352,24 @@ } } }, + "Saved to Documents/InsightsExports" : { + "comment" : "A description of where the insights export file will be saved.", + "isCommentAutoGenerated" : true + }, + "Saved to Documents/SharingExports" : { + "comment" : "A description of where the generated sharing screenshots are saved on a user's device.", + "isCommentAutoGenerated" : true + }, + "Saved to Documents/VotingLayoutExports" : { + "comment" : "A description of where the voting layouts are saved when exported.", + "isCommentAutoGenerated" : true + }, + "Saved to Documents/WatchExports" : { + "comment" : "A description of where the exported watch views are saved.", + "isCommentAutoGenerated" : true + }, "Saved to Documents/WidgetExports" : { "comment" : "A description of where the exported widget screenshots are saved.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -13491,7 +13535,6 @@ } }, "Select Style" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -13617,7 +13660,6 @@ }, "Send 5 personality pack notifications" : { "comment" : "A description of the action that can be performed when tapping the \"Test All Notifications\" button in the Settings app.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -14354,7 +14396,6 @@ }, "Shown This Session" : { "comment" : "A label displaying whether they have seen a tip during the current session.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -15168,6 +15209,9 @@ } } } + }, + "Tap to log mood" : { + }, "Tap to log mood for this day" : { "comment" : "A hint that appears when a user taps on a day with no mood logged, instructing them to log a mood.", @@ -15297,7 +15341,6 @@ }, "Tap to preview" : { "comment" : "A text label displayed above a list of tips, instructing the user to tap on an item to view more details.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -15465,7 +15508,6 @@ } }, "Tap to vote" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15507,7 +15549,6 @@ }, "Test All Notifications" : { "comment" : "A button label that tests sending notifications.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -15550,7 +15591,6 @@ }, "Test builds only" : { "comment" : "A section header that indicates that the settings view contains only test data.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -15634,7 +15674,6 @@ } }, "THE ART\nOF FEELING" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15842,7 +15881,6 @@ }, "Tips Enabled" : { "comment" : "A toggle that enables or disables tips in the app.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -15885,7 +15923,6 @@ }, "Tips Preview" : { "comment" : "A label for a view that previews all tip modals.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -16852,7 +16889,6 @@ }, "View all tip modals" : { "comment" : "A description of what the \"Tips Preview\" button does.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "de" : { @@ -16894,7 +16930,6 @@ } }, "View Full Paywall" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -17486,7 +17521,6 @@ } }, "Write Your\nEmotional Story" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/FeelsTests/DayViewViewModelTests.swift b/FeelsTests/DayViewViewModelTests.swift new file mode 100644 index 0000000..5e8de99 --- /dev/null +++ b/FeelsTests/DayViewViewModelTests.swift @@ -0,0 +1,42 @@ +// +// DayViewViewModelTests.swift +// FeelsTests +// +// Unit tests for DayViewViewModel.countEntries — verifies safe counting +// across various grouped dictionary shapes (TDD for force-unwrap fix). +// + +import XCTest +import SwiftData +@testable import Feels + +@MainActor +final class DayViewViewModelTests: XCTestCase { + + func testCountEntries_EmptyGrouped() { + let grouped: [Int: [Int: [MoodEntryModel]]] = [:] + XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 0) + } + + func testCountEntries_SingleYearSingleMonth() { + let entry = MoodEntryModel(forDate: Date(), mood: .great, entryType: .listView) + let grouped: [Int: [Int: [MoodEntryModel]]] = [2026: [2: [entry]]] + XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 1) + } + + func testCountEntries_MultipleYearsAndMonths() { + let e1 = MoodEntryModel(forDate: Date(), mood: .great, entryType: .listView) + let e2 = MoodEntryModel(forDate: Date(), mood: .good, entryType: .listView) + let e3 = MoodEntryModel(forDate: Date(), mood: .bad, entryType: .listView) + let grouped: [Int: [Int: [MoodEntryModel]]] = [ + 2025: [12: [e1, e2]], + 2026: [1: [e3], 2: [e1, e2, e3]] + ] + XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 6) + } + + func testCountEntries_YearWithEmptyMonth() { + let grouped: [Int: [Int: [MoodEntryModel]]] = [2026: [1: []]] + XCTAssertEqual(DayViewViewModel.countEntries(in: grouped), 0) + } +} diff --git a/Shared/MoodStreakActivity.swift b/Shared/MoodStreakActivity.swift index e622da5..d150bd1 100644 --- a/Shared/MoodStreakActivity.swift +++ b/Shared/MoodStreakActivity.swift @@ -112,9 +112,10 @@ class LiveActivityManager: ObservableObject { // End activity at midnight func scheduleActivityEnd() { - guard let activity = currentActivity else { return } + guard let activity = currentActivity, + let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) else { return } - let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) + let midnight = Calendar.current.startOfDay(for: tomorrow) Task { await activity.end(nil, dismissalPolicy: .after(midnight)) diff --git a/Shared/Services/HealthService.swift b/Shared/Services/HealthService.swift index 538e9d6..a94fdb1 100644 --- a/Shared/Services/HealthService.swift +++ b/Shared/Services/HealthService.swift @@ -31,21 +31,21 @@ class HealthService: ObservableObject { // MARK: - Data Types // Core activity metrics - private let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount)! - private let exerciseTimeType = HKQuantityType.quantityType(forIdentifier: .appleExerciseTime)! - private let activeEnergyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! - private let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)! + private let stepCountType = HKQuantityType(.stepCount) + private let exerciseTimeType = HKQuantityType(.appleExerciseTime) + private let activeEnergyType = HKQuantityType(.activeEnergyBurned) + private let distanceType = HKQuantityType(.distanceWalkingRunning) // Heart & stress indicators - private let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)! - private let restingHeartRateType = HKQuantityType.quantityType(forIdentifier: .restingHeartRate)! - private let hrvType = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)! + private let heartRateType = HKQuantityType(.heartRate) + private let restingHeartRateType = HKQuantityType(.restingHeartRate) + private let hrvType = HKQuantityType(.heartRateVariabilitySDNN) // Sleep & recovery - private let sleepAnalysisType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis)! + private let sleepAnalysisType = HKCategoryType(.sleepAnalysis) // Mindfulness - private let mindfulSessionType = HKCategoryType.categoryType(forIdentifier: .mindfulSession)! + private let mindfulSessionType = HKCategoryType(.mindfulSession) // State of Mind private let stateOfMindType = HKSampleType.stateOfMindType() @@ -117,7 +117,9 @@ class HealthService: ObservableObject { func fetchHealthData(for date: Date) async -> DailyHealthData { let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: date) - let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)! + guard let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) else { + return DailyHealthData(date: date, steps: nil, exerciseMinutes: nil, activeCalories: nil, distanceKm: nil, averageHeartRate: nil, restingHeartRate: nil, hrv: nil, sleepHours: nil, mindfulMinutes: nil) + } // Fetch all metrics concurrently async let steps = fetchSteps(start: startOfDay, end: endOfDay) @@ -233,8 +235,10 @@ class HealthService: ObservableObject { // Sleep data is typically recorded for the night before // So for mood on date X, we look at sleep from evening of X-1 to morning of X let calendar = Calendar.current - let endOfSleep = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: date)! - let startOfSleep = calendar.date(byAdding: .hour, value: -18, to: endOfSleep)! + guard let endOfSleep = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: date), + let startOfSleep = calendar.date(byAdding: .hour, value: -18, to: endOfSleep) else { + return nil + } let predicate = HKQuery.predicateForSamples(withStart: startOfSleep, end: endOfSleep, options: .strictStartDate) diff --git a/Shared/Views/CelebrationAnimations.swift b/Shared/Views/CelebrationAnimations.swift index 18817b5..31ac11b 100644 --- a/Shared/Views/CelebrationAnimations.swift +++ b/Shared/Views/CelebrationAnimations.swift @@ -106,7 +106,8 @@ struct CelebrationOverlayView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { hasAppeared = true - DispatchQueue.main.asyncAfter(deadline: .now() + animationType.duration) { + Task { @MainActor in + try? await Task.sleep(for: .seconds(animationType.duration)) onComplete() } } @@ -341,7 +342,8 @@ struct ShatterReformAnimation: View { } // Phase 2: Converge to center and fade - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + Task { @MainActor in + try? await Task.sleep(for: .seconds(0.6)) phase = .reform withAnimation(.easeInOut(duration: 0.5)) { for i in 0.. Int { + grouped.values.flatMap(\.values).reduce(0) { $0 + $1.count } } init(addMonthStartWeekdayPadding: Bool) { @@ -90,8 +87,8 @@ class DayViewViewModel: ObservableObject { let forDate = entry.forDate let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate) - let month = components.month! - let year = components.year! + let month = components.month ?? 1 + let year = components.year ?? Calendar.current.component(.year, from: Date()) let monthName = Random.monthName(fromMonthInt: month) let weekday = Random.weekdayName(fromDate: entry.forDate) diff --git a/Shared/Views/HeaderPercView.swift b/Shared/Views/HeaderPercView.swift index a0b956a..b7ebee2 100644 --- a/Shared/Views/HeaderPercView.swift +++ b/Shared/Views/HeaderPercView.swift @@ -32,10 +32,10 @@ struct HeaderPercView: View { if fakeData { moodEntries = DataController.shared.randomEntries(count: 10) } else { - var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())! - daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)! - - moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) + if let daysAgoRaw = Calendar.current.date(byAdding: .day, value: -backDays, to: Date()), + let daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgoRaw) { + moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) + } } if let moodEntries = moodEntries { @@ -57,6 +57,7 @@ struct HeaderPercView: View { .fontWeight(.bold) .foregroundColor(moodTint.color(forMood: model.mood)) .frame(maxWidth: .infinity) + .accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent") } } Spacer() @@ -67,6 +68,7 @@ struct HeaderPercView: View { .fontWeight(.bold) .foregroundColor(moodTint.color(forMood: model.mood)) .frame(maxWidth: .infinity) + .accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent") } } Spacer() @@ -82,6 +84,7 @@ struct HeaderPercView: View { bgColor: moodTint.color(forMood: model.mood), textColor: textColor) .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent") } } Spacer() @@ -91,6 +94,7 @@ struct HeaderPercView: View { bgColor: moodTint.color(forMood: model.mood), textColor: textColor) .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent") } } Spacer() diff --git a/Shared/Views/SharingTemplates/MonthTotalTemplate.swift b/Shared/Views/SharingTemplates/MonthTotalTemplate.swift index 95400ee..5e07e1a 100644 --- a/Shared/Views/SharingTemplates/MonthTotalTemplate.swift +++ b/Shared/Views/SharingTemplates/MonthTotalTemplate.swift @@ -17,7 +17,7 @@ struct MonthTotalTemplate: View, SharingTemplate { var endDate: Date var totalEntryCount: Int = 0 - private var month = Calendar.current.dateComponents([.month], from: Date()).month! + private var month = Calendar.current.dateComponents([.month], from: Date()).month ?? 1 @State var showSharingTemplate = false @StateObject private var shareImage = ShareImageStateViewModel() diff --git a/Shared/Views/SmallRollUpHeaderView.swift b/Shared/Views/SmallRollUpHeaderView.swift index 66b9682..30781d1 100644 --- a/Shared/Views/SmallRollUpHeaderView.swift +++ b/Shared/Views/SmallRollUpHeaderView.swift @@ -43,6 +43,7 @@ struct SmallRollUpHeaderView: View { .font(.title2) .fontWeight(.bold) .lineLimit(1) + .minimumScaleFactor(0.5) .foregroundColor(moodTint.color(forMood: model.mood)) .frame(maxWidth: .infinity, alignment: .center) } diff --git a/Tests iOS/Tests_iOS.swift b/Tests iOS/Tests_iOS.swift index 1456654..10563f6 100644 --- a/Tests iOS/Tests_iOS.swift +++ b/Tests iOS/Tests_iOS.swift @@ -6,7 +6,7 @@ // import XCTest -@testable import iFeel +@testable import Feels class Tests_iOS: XCTestCase { override func setUpWithError() throws { @@ -49,9 +49,10 @@ class Tests_iOS: XCTestCase { // let fakeOnboarding = OnboardingData() // fakeOnboarding.inputDay = DayOptions.Today // fakeOnboarding.date = todayOneHourAhead -// +// // let lastDay = ShowBasedOnVoteLogics.getLastDateVoteShouldExist(onboardingData: fakeOnboarding) // let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())! // XCTAssertTrue(lastDay == yesterday) // } + }