Fix 8 audit items: remove force-unwraps, improve accessibility and concurrency

- 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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-17 11:42:16 -06:00
parent 277e277750
commit 7f27446b94
11 changed files with 168 additions and 76 deletions

View File

@@ -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 = "<group>"; };
5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataControllerCRUDTests.swift; sourceTree = "<group>"; };
9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DayViewViewModelTests.swift; sourceTree = "<group>"; };
B60015D02A064FF582E232FD /* Feels Watch App/Feels Watch AppDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch AppDebug.entitlements"; sourceTree = "<group>"; };
B8AB4CD73C2B4DC89C6FE84D /* Feels Watch App/Feels Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch App.entitlements"; sourceTree = "<group>"; };
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 */,

View File

@@ -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" : {

View File

@@ -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)
}
}

View File

@@ -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))

View File

@@ -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)

View File

@@ -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..<shardCount {
@@ -353,14 +355,16 @@ struct ShatterReformAnimation: View {
}
// Phase 3: Show checkmark
DispatchQueue.main.asyncAfter(deadline: .now() + 1.1) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.1))
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
checkmarkOpacity = 1
}
}
// Phase 4: Fade out
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.8))
withAnimation(.easeOut(duration: 0.3)) {
checkmarkOpacity = 0
}

View File

@@ -20,15 +20,12 @@ class DayViewViewModel: ObservableObject {
}
private var numberOfEntries: Int {
var num = 0
grouped.keys.forEach({
let year = grouped[$0]
let monthKeys = year?.keys
monthKeys?.forEach({
num += year![$0]!.count
})
})
return num
Self.countEntries(in: grouped)
}
/// Count total entries across all year/month groups. Extracted for testability.
static func countEntries(in grouped: [Int: [Int: [MoodEntryModel]]]) -> 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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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)
// }
}