Add interactive widget voting and fix warnings/bugs

Widget Features:
- Add inline voting to timeline widget when no entry exists for today
- Show random prompt from notification strings in voting mode
- Update vote widget to use simple icon style for selection
- Make stats bar full width in voted state view
- Add Localizable.strings to widget extension target

Bug Fixes:
- Fix inverted date calculation in InsightsViewModel streak logic
- Replace force unwraps with safe optional handling in widgets
- Replace fatalError calls with graceful error handling
- Fix CSV import safety in SettingsView

Warning Fixes:
- Add @retroactive to Color and Date extension conformances
- Update deprecated onChange(of:perform:) to new syntax
- Replace deprecated applicationIconBadgeNumber with setBadgeCount
- Replace deprecated UIApplication.shared.windows API
- Add @preconcurrency for Swift 6 protocol conformances
- Add missing widget family cases to switch statement
- Remove unused variables and #warning directives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-10 16:23:12 -06:00
parent aaaf04f05e
commit f822927e98
20 changed files with 317 additions and 220 deletions

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1C0DAB45279DB0FB003B1F21 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB47279DB0FB003B1F21 /* Localizable.strings */; }; 1C0DAB45279DB0FB003B1F21 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB47279DB0FB003B1F21 /* Localizable.strings */; };
1C0DAB49279DB0FB003B1F22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB47279DB0FB003B1F21 /* Localizable.strings */; };
1C2618FA2795E41D00FDC148 /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 1C2618F92795E41D00FDC148 /* Charts */; }; 1C2618FA2795E41D00FDC148 /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 1C2618F92795E41D00FDC148 /* Charts */; };
1C747CC9279F06EB00762CBD /* CloudKitSyncMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 1C747CC8279F06EB00762CBD /* CloudKitSyncMonitor */; }; 1C747CC9279F06EB00762CBD /* CloudKitSyncMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 1C747CC8279F06EB00762CBD /* CloudKitSyncMonitor */; };
1CB4D0A028787D8A00902A56 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB4D09F28787D8A00902A56 /* StoreKit.framework */; }; 1CB4D0A028787D8A00902A56 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB4D09F28787D8A00902A56 /* StoreKit.framework */; };
@@ -436,6 +437,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
1C0DAB49279DB0FB003B1F22 /* Localizable.strings in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -52,14 +52,16 @@ struct VoteWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> VoteWidgetEntry { func placeholder(in context: Context) -> VoteWidgetEntry {
// Show sample "already voted" state for widget picker preview // Show sample "already voted" state for widget picker preview
let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]) let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])
return VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats) let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
return VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText)
} }
func getSnapshot(in context: Context, completion: @escaping (VoteWidgetEntry) -> Void) { func getSnapshot(in context: Context, completion: @escaping (VoteWidgetEntry) -> Void) {
// Show sample data for widget picker preview // Show sample data for widget picker preview
if context.isPreview { if context.isPreview {
let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]) let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])
let entry = VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats) let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
let entry = VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText)
completion(entry) completion(entry)
return return
} }
@@ -115,12 +117,16 @@ struct VoteWidgetProvider: TimelineProvider {
} }
} }
// Get random prompt text for voting view
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
return VoteWidgetEntry( return VoteWidgetEntry(
date: Date(), date: Date(),
hasSubscription: hasSubscription, hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday, hasVotedToday: hasVotedToday,
todaysMood: todaysMood, todaysMood: todaysMood,
stats: stats stats: stats,
promptText: promptText
) )
} }
} }
@@ -145,6 +151,7 @@ struct VoteWidgetEntry: TimelineEntry {
let hasVotedToday: Bool let hasVotedToday: Bool
let todaysMood: Mood? let todaysMood: Mood?
let stats: MoodStats? let stats: MoodStats?
let promptText: String
} }
// MARK: - Widget Views // MARK: - Widget Views
@@ -161,7 +168,7 @@ struct FeelsVoteWidgetEntryView: View {
VotedStatsView(entry: entry) VotedStatsView(entry: entry)
} else { } else {
// Show voting buttons // Show voting buttons
VotingView(family: family) VotingView(family: family, promptText: entry.promptText)
} }
} else { } else {
// Non-subscriber view - tap to open app // Non-subscriber view - tap to open app
@@ -176,39 +183,9 @@ struct FeelsVoteWidgetEntryView: View {
struct VotingView: View { struct VotingView: View {
let family: WidgetFamily let family: WidgetFamily
let promptText: String
let moods: [Mood] = [.horrible, .bad, .average, .good, .great] let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
var body: some View {
VStack(spacing: 8) {
Text("How are you feeling?")
.font(.headline)
.foregroundStyle(.primary)
if family == .systemSmall {
// Compact layout for small widget
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
ForEach(moods, id: \.rawValue) { mood in
MoodButton(mood: mood, isCompact: true)
}
}
} else {
// Horizontal layout for medium/large
HStack(spacing: 4) {
ForEach(moods, id: \.rawValue) { mood in
MoodButton(mood: mood, isCompact: false)
.frame(maxWidth: .infinity)
}
}
}
}
.padding()
}
}
struct MoodButton: View {
let mood: Mood
let isCompact: Bool
private var moodTint: MoodTintable.Type { private var moodTint: MoodTintable.Type {
UserDefaultsStore.moodTintable() UserDefaultsStore.moodTintable()
} }
@@ -218,23 +195,28 @@ struct MoodButton: View {
} }
var body: some View { var body: some View {
Button(intent: VoteMoodIntent(mood: mood)) { VStack(spacing: 12) {
VStack(spacing: 4) { Text(promptText)
moodImages.icon(forMood: mood) .font(.headline)
.resizable() .foregroundStyle(.primary)
.aspectRatio(contentMode: .fit) .multilineTextAlignment(.center)
.frame(width: isCompact ? 28 : 36, height: isCompact ? 28 : 36) .lineLimit(2)
.foregroundColor(moodTint.color(forMood: mood)) .minimumScaleFactor(0.8)
if !isCompact { HStack(spacing: 8) {
Text(mood.widgetDisplayName) ForEach(moods, id: \.rawValue) { mood in
.font(.caption2) Button(intent: VoteMoodIntent(mood: mood)) {
.foregroundStyle(.secondary) moodImages.icon(forMood: mood)
.lineLimit(1) .resizable()
.minimumScaleFactor(0.7) .aspectRatio(contentMode: .fit)
.frame(width: family == .systemSmall ? 36 : 44, height: family == .systemSmall ? 36 : 44)
.foregroundColor(moodTint.color(forMood: mood))
}
.buttonStyle(.plain)
} }
} }
} }
.padding()
} }
} }
@@ -284,17 +266,19 @@ struct VotedStatsView: View {
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
HStack(spacing: 4) { GeometryReader { geo in
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in HStack(spacing: 2) {
let percentage = stats.percentage(for: mood) ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
if percentage > 0 { let percentage = stats.percentage(for: mood)
RoundedRectangle(cornerRadius: 2) if percentage > 0 {
.fill(moodTint.color(forMood: mood)) RoundedRectangle(cornerRadius: 2)
.frame(width: max(4, CGFloat(percentage) * 0.8)) .fill(moodTint.color(forMood: mood))
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
}
} }
} }
} }
.frame(height: 8) .frame(height: 12)
} }
} }
} }
@@ -347,7 +331,7 @@ struct FeelsVoteWidget: Widget {
#Preview(as: .systemSmall) { #Preview(as: .systemSmall) {
FeelsVoteWidget() FeelsVoteWidget()
} timeline: { } timeline: {
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil) VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling today?")
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .great, stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1])) VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .great, stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]), promptText: "")
VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil) VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "")
} }

View File

@@ -86,7 +86,7 @@ struct TimeLineCreator {
} }
} }
struct Provider: IntentTimelineProvider { struct Provider: @preconcurrency IntentTimelineProvider {
let timeLineCreator = TimeLineCreator() let timeLineCreator = TimeLineCreator()
/* /*
@@ -107,25 +107,52 @@ struct Provider: IntentTimelineProvider {
} else { } else {
timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))
} }
let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus()
let entry = SimpleEntry(date: Date(), let entry = SimpleEntry(date: Date(),
configuration: ConfigurationIntent(), configuration: ConfigurationIntent(),
timeLineViews: timeLineViews) timeLineViews: timeLineViews,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
promptText: promptText)
completion(entry) completion(entry)
} }
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { @MainActor func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus()
let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!, let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!,
configuration: ConfigurationIntent(), configuration: ConfigurationIntent(),
timeLineViews: nil) timeLineViews: nil,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
promptText: promptText)
let midNightEntry = SimpleEntry(date: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())!, let midNightEntry = SimpleEntry(date: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())!,
configuration: ConfigurationIntent(), configuration: ConfigurationIntent(),
timeLineViews: nil) timeLineViews: nil,
hasSubscription: hasSubscription,
hasVotedToday: hasVotedToday,
promptText: promptText)
let date = Calendar.current.date(byAdding: .second, value: 10, to: Date())! let date = Calendar.current.date(byAdding: .second, value: 10, to: Date())!
let timeline = Timeline(entries: [entry, midNightEntry], policy: .after(date)) let timeline = Timeline(entries: [entry, midNightEntry], policy: .after(date))
completion(timeline) completion(timeline)
} }
@MainActor
private func checkSubscriptionAndVoteStatus() -> (hasSubscription: Bool, hasVotedToday: Bool, promptText: String) {
let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
let dayStart = Calendar.current.startOfDay(for: votingDate)
let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart) ?? dayStart
let todayEntry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first
let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder
let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body
return (hasSubscription, hasVotedToday, promptText)
}
} }
struct SimpleEntry: TimelineEntry { struct SimpleEntry: TimelineEntry {
@@ -133,12 +160,18 @@ struct SimpleEntry: TimelineEntry {
let configuration: ConfigurationIntent let configuration: ConfigurationIntent
let timeLineViews: [WatchTimelineView]? let timeLineViews: [WatchTimelineView]?
let showStats: Bool let showStats: Bool
let hasSubscription: Bool
init(date: Date, configuration: ConfigurationIntent, timeLineViews: [WatchTimelineView]?, showStats: Bool = false) { let hasVotedToday: Bool
let promptText: String
init(date: Date, configuration: ConfigurationIntent, timeLineViews: [WatchTimelineView]?, showStats: Bool = false, hasSubscription: Bool = false, hasVotedToday: Bool = true, promptText: String = "") {
self.date = date self.date = date
self.configuration = configuration self.configuration = configuration
self.timeLineViews = timeLineViews self.timeLineViews = timeLineViews
self.showStats = showStats self.showStats = showStats
self.hasSubscription = hasSubscription
self.hasVotedToday = hasVotedToday
self.promptText = promptText
} }
} }
@@ -146,13 +179,16 @@ struct SimpleEntry: TimelineEntry {
struct FeelsWidgetEntryView : View { struct FeelsWidgetEntryView : View {
@Environment(\.sizeCategory) var sizeCategory @Environment(\.sizeCategory) var sizeCategory
@Environment(\.widgetFamily) var family @Environment(\.widgetFamily) var family
var entry: Provider.Entry var entry: Provider.Entry
private var showVotingForToday: Bool {
entry.hasSubscription && !entry.hasVotedToday
}
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
ZStack { Group {
Color(UIColor.systemBackground)
switch family { switch family {
case .systemSmall: case .systemSmall:
SmallWidgetView(entry: entry) SmallWidgetView(entry: entry)
@@ -162,10 +198,13 @@ struct FeelsWidgetEntryView : View {
LargeWidgetView(entry: entry) LargeWidgetView(entry: entry)
case .systemExtraLarge: case .systemExtraLarge:
LargeWidgetView(entry: entry) LargeWidgetView(entry: entry)
case .accessoryCircular, .accessoryRectangular, .accessoryInline:
SmallWidgetView(entry: entry)
@unknown default: @unknown default:
fatalError() MediumWidgetView(entry: entry)
} }
} }
.containerBackground(showVotingForToday ? Color.clear : Color(UIColor.systemBackground), for: .widget)
} }
} }
@@ -181,7 +220,9 @@ struct SmallWidgetView: View {
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
return view.color != moodTint.color(forMood: .missing) return view.color != moodTint.color(forMood: .missing)
} }
timeLineView = hasRealData ? [realData.first!] : [TimeLineCreator.createSampleViews(count: 1).first!] if let firstView = hasRealData ? realData.first : TimeLineCreator.createSampleViews(count: 1).first {
timeLineView = [firstView]
}
} }
var body: some View { var body: some View {
@@ -204,6 +245,10 @@ struct MediumWidgetView: View {
var entry: Provider.Entry var entry: Provider.Entry
var timeLineView = [WatchTimelineView]() var timeLineView = [WatchTimelineView]()
private var showVotingForToday: Bool {
entry.hasSubscription && !entry.hasVotedToday
}
init(entry: Provider.Entry) { init(entry: Provider.Entry) {
self.entry = entry self.entry = entry
let realData = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5)) let realData = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5))
@@ -219,13 +264,15 @@ struct MediumWidgetView: View {
VStack { VStack {
Spacer() Spacer()
TimeHeaderView(startDate: timeLineView.first!.date, endDate: timeLineView.last!.date) if !showVotingForToday, let first = timeLineView.first, let last = timeLineView.last {
.frame(minWidth: 0, maxWidth: .infinity) TimeHeaderView(startDate: first.date, endDate: last.date)
.multilineTextAlignment(.leading) .frame(minWidth: 0, maxWidth: .infinity)
.multilineTextAlignment(.leading)
}
TimeBodyView(group: timeLineView) TimeBodyView(group: timeLineView, showVotingForToday: showVotingForToday, promptText: entry.promptText)
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: showVotingForToday ? 0 : 25, style: .continuous))
.frame(minHeight: 0, maxHeight: 55) .frame(minHeight: 0, maxHeight: showVotingForToday ? 80 : 55)
.padding() .padding()
Spacer() Spacer()
@@ -237,6 +284,10 @@ struct LargeWidgetView: View {
var entry: Provider.Entry var entry: Provider.Entry
var timeLineView = [WatchTimelineView]() var timeLineView = [WatchTimelineView]()
private var showVotingForToday: Bool {
entry.hasSubscription && !entry.hasVotedToday
}
init(entry: Provider.Entry) { init(entry: Provider.Entry) {
self.entry = entry self.entry = entry
let realData = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) let realData = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))
@@ -248,33 +299,52 @@ struct LargeWidgetView: View {
timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10) timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10)
} }
var firstGroup: ([WatchTimelineView], String) { var firstGroup: [WatchTimelineView] {
return (Array(self.timeLineView.prefix(5)), UUID().uuidString) return Array(self.timeLineView.prefix(5))
} }
var secondGroup: ([WatchTimelineView], String) { var secondGroup: [WatchTimelineView] {
return (Array(self.timeLineView.suffix(5)), UUID().uuidString) return Array(self.timeLineView.suffix(5))
} }
var body: some View { var body: some View {
VStack { VStack {
Spacer() Spacer()
ForEach([firstGroup, secondGroup], id: \.1) { group in // First row (includes today - may show voting)
VStack { VStack {
Spacer() Spacer()
TimeHeaderView(startDate: group.0.first!.date, endDate: group.0.last!.date) if !showVotingForToday, let first = firstGroup.first, let last = firstGroup.last {
TimeHeaderView(startDate: first.date, endDate: last.date)
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
TimeBodyView(group: group.0)
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
.frame(minHeight: 0, maxHeight: 55)
.padding()
Spacer()
} }
TimeBodyView(group: firstGroup, showVotingForToday: showVotingForToday, promptText: entry.promptText)
.clipShape(RoundedRectangle(cornerRadius: showVotingForToday ? 0 : 25, style: .continuous))
.frame(minHeight: 0, maxHeight: showVotingForToday ? 80 : 55)
.padding()
Spacer()
}
// Second row (older entries - never show voting)
VStack {
Spacer()
if let first = secondGroup.first, let last = secondGroup.last {
TimeHeaderView(startDate: first.date, endDate: last.date)
.frame(minWidth: 0, maxWidth: .infinity)
.multilineTextAlignment(.leading)
}
TimeBodyView(group: secondGroup, showVotingForToday: false)
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
.frame(minHeight: 0, maxHeight: 55)
.padding()
Spacer()
} }
Spacer() Spacer()
@@ -376,16 +446,63 @@ struct TimeHeaderView: View {
struct TimeBodyView: View { struct TimeBodyView: View {
let group: [WatchTimelineView] let group: [WatchTimelineView]
var showVotingForToday: Bool = false
var promptText: String = ""
var body: some View { var body: some View {
ZStack { if showVotingForToday {
Color(UIColor.secondarySystemBackground) // Show voting view without extra background container
HStack { InlineVotingView(promptText: promptText)
ForEach(group) { watchView in .padding()
EntryCard(timeLineView: watchView) } else {
ZStack {
Color(UIColor.secondarySystemBackground)
HStack(spacing: 4) {
ForEach(group) { watchView in
EntryCard(timeLineView: watchView)
}
}
.padding()
}
}
}
}
// MARK: - Inline Voting View (compact mood buttons for timeline widget)
struct InlineVotingView: View {
let promptText: String
let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
private var moodTint: MoodTintable.Type {
UserDefaultsStore.moodTintable()
}
private var moodImages: MoodImagable.Type {
UserDefaultsStore.moodMoodImagable()
}
var body: some View {
VStack(spacing: 8) {
Text(promptText)
.font(.subheadline)
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
.lineLimit(2)
.minimumScaleFactor(0.7)
HStack(spacing: 8) {
ForEach(moods, id: \.rawValue) { mood in
Button(intent: VoteMoodIntent(mood: mood)) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 44, height: 44)
.foregroundColor(moodTint.color(forMood: mood))
}
.buttonStyle(.plain)
} }
} }
.padding()
} }
} }
} }

View File

@@ -45,7 +45,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
} }
} }
extension AppDelegate: UNUserNotificationCenterDelegate { extension AppDelegate: @preconcurrency UNUserNotificationCenterDelegate {
func requestAuthorization() { } func requestAuthorization() { }
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
@@ -68,7 +68,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
case .great: case .great:
DataController.shared.add(mood: .great, forDate: date, entryType: .notification) DataController.shared.add(mood: .great, forDate: date, entryType: .notification)
} }
UIApplication.shared.applicationIconBadgeNumber = 0 UNUserNotificationCenter.current().setBadgeCount(0)
} }
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()
completionHandler() completionHandler()

View File

@@ -10,7 +10,7 @@ import SwiftUI
// Inspired by https://cocoacasts.com/from-hex-to-uicolor-and-back-in-swift // Inspired by https://cocoacasts.com/from-hex-to-uicolor-and-back-in-swift
// Make Color codable. This includes support for transparency. // Make Color codable. This includes support for transparency.
// See https://www.digitalocean.com/community/tutorials/css-hex-code-colors-alpha-values // See https://www.digitalocean.com/community/tutorials/css-hex-code-colors-alpha-values
extension Color: Codable { extension Color: @retroactive Codable {
init(hex: String) { init(hex: String) {
let rgba = hex.toRGBA() let rgba = hex.toRGBA()
@@ -99,7 +99,7 @@ extension String {
} }
} }
extension Color: RawRepresentable { extension Color: @retroactive RawRepresentable {
// TODO: Sort out alpha // TODO: Sort out alpha
public init?(rawValue: Int) { public init?(rawValue: Int) {
let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF

View File

@@ -7,7 +7,7 @@
import Foundation import Foundation
extension Date: RawRepresentable { extension Date: @retroactive RawRepresentable {
public var rawValue: String { public var rawValue: String {
self.timeIntervalSinceReferenceDate.description self.timeIntervalSinceReferenceDate.description
} }

View File

@@ -25,7 +25,7 @@ struct FeelsApp: App {
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask) BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)
} }
UIApplication.shared.applicationIconBadgeNumber = 0 UNUserNotificationCenter.current().setBadgeCount(0)
} }
var body: some Scene { var body: some Scene {
@@ -46,14 +46,14 @@ struct FeelsApp: App {
showSubscriptionFromWidget = true showSubscriptionFromWidget = true
} }
} }
}.onChange(of: scenePhase) { phase in }.onChange(of: scenePhase) { _, newPhase in
if phase == .background { if newPhase == .background {
//BGTask.scheduleBackgroundProcessing() //BGTask.scheduleBackgroundProcessing()
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()
} }
if phase == .active { if newPhase == .active {
UIApplication.shared.applicationIconBadgeNumber = 0 UNUserNotificationCenter.current().setBadgeCount(0)
// Check subscription status on each app launch // Check subscription status on each app launch
Task { Task {
await iapManager.checkSubscriptionStatus() await iapManager.checkSubscriptionStatus()

View File

@@ -167,8 +167,6 @@ class IAPManager: ObservableObject {
} }
private func checkForActiveSubscription() async -> Bool { private func checkForActiveSubscription() async -> Bool {
var foundActiveSubscription = false
for await result in Transaction.currentEntitlements { for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue } guard case .verified(let transaction) = result else { continue }
@@ -178,9 +176,6 @@ class IAPManager: ObservableObject {
// Check if this is one of our subscription products // Check if this is one of our subscription products
guard productIdentifiers.contains(transaction.productID) else { continue } guard productIdentifiers.contains(transaction.productID) else { continue }
// Found an active subscription
foundActiveSubscription = true
// Get the product for this transaction // Get the product for this transaction
currentProduct = availableProducts.first { $0.id == transaction.productID } currentProduct = availableProducts.first { $0.id == transaction.productID }

View File

@@ -62,14 +62,15 @@ class UserDefaultsStore {
} }
} }
@discardableResult
static func saveOnboarding(onboardingData: OnboardingData) -> OnboardingData { static func saveOnboarding(onboardingData: OnboardingData) -> OnboardingData {
do { do {
let data = try JSONEncoder().encode(onboardingData) let data = try JSONEncoder().encode(onboardingData)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue)
return UserDefaultsStore.getOnboarding()
} catch { } catch {
fatalError("error saving") print("Error saving onboarding: \(error)")
} }
return UserDefaultsStore.getOnboarding()
} }
static func moodMoodImagable() -> MoodImagable.Type { static func moodMoodImagable() -> MoodImagable.Type {
@@ -114,21 +115,21 @@ class UserDefaultsStore {
return model return model
} else { } else {
GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.customWidget.rawValue) GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.customWidget.rawValue)
let widget = CustomWidgetModel.randomWidget let widget = CustomWidgetModel.randomWidget
widget.isSaved = true widget.isSaved = true
let widgets = [widget] let widgets = [widget]
let data = try! JSONEncoder().encode(widgets)
guard let data = try? JSONEncoder().encode(widgets) else {
return widgets
}
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data, if let savedData = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data,
let models = try? JSONDecoder().decode([CustomWidgetModel].self, from: data) { let models = try? JSONDecoder().decode([CustomWidgetModel].self, from: savedData) {
let sorted = models.sorted(by: { return models.sorted { $0.createdDate < $1.createdDate }
$0.createdDate < $1.createdDate
})
return sorted
} else { } else {
fatalError("error getting widgets") return widgets
} }
} }
} }
@@ -160,12 +161,12 @@ class UserDefaultsStore {
}) })
let data = try JSONEncoder().encode(existingWidgets) let data = try JSONEncoder().encode(existingWidgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
return UserDefaultsStore.getCustomWidgets()
} catch { } catch {
fatalError("error saving") print("Error saving custom widget: \(error)")
} }
return UserDefaultsStore.getCustomWidgets()
} }
@discardableResult @discardableResult
static func deleteCustomWidget(withUUID uuid: String) -> [CustomWidgetModel] { static func deleteCustomWidget(withUUID uuid: String) -> [CustomWidgetModel] {
do { do {
@@ -184,22 +185,18 @@ class UserDefaultsStore {
existingWidgets.append(widget) existingWidgets.append(widget)
} }
if let _ = existingWidgets.first(where: { if existingWidgets.first(where: { $0.inUse == true }) == nil {
$0.inUse == true existingWidgets.first?.inUse = true
}) {} else {
if let first = existingWidgets.first {
first.inUse = true
}
} }
let data = try JSONEncoder().encode(existingWidgets) let data = try JSONEncoder().encode(existingWidgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
return UserDefaultsStore.getCustomWidgets()
} catch { } catch {
fatalError("error saving") print("Error deleting custom widget: \(error)")
} }
return UserDefaultsStore.getCustomWidgets()
} }
static func getCustomMoodTint() -> SavedMoodTint { static func getCustomMoodTint() -> SavedMoodTint {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customMoodTint.rawValue) as? Data{ if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customMoodTint.rawValue) as? Data{
do { do {
@@ -226,11 +223,10 @@ class UserDefaultsStore {
do { do {
let data = try JSONEncoder().encode(customTint) let data = try JSONEncoder().encode(customTint)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customMoodTint.rawValue) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customMoodTint.rawValue)
return UserDefaultsStore.getCustomMoodTint()
} catch { } catch {
print(error) print("Error saving custom mood tint: \(error)")
fatalError("error saving")
} }
return UserDefaultsStore.getCustomMoodTint()
} }
@discardableResult @discardableResult

View File

@@ -14,12 +14,12 @@ extension DataController {
try modelContext.delete(model: MoodEntryModel.self) try modelContext.delete(model: MoodEntryModel.self)
saveAndRunDataListeners() saveAndRunDataListeners()
} catch { } catch {
fatalError("Failed to clear database: \(error)") print("Failed to clear database: \(error)")
} }
} }
func deleteLast(numberOfEntries: Int) { func deleteLast(numberOfEntries: Int) {
let startDate = Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date())! guard let startDate = Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date()) else { return }
let entries = getData(startDate: startDate, endDate: Date(), includedDays: []) let entries = getData(startDate: startDate, endDate: Date(), includedDays: [])
for entry in entries { for entry in entries {
@@ -29,7 +29,7 @@ extension DataController {
} }
func deleteRandomFromLast(numberOfEntries: Int) { func deleteRandomFromLast(numberOfEntries: Int) {
let startDate = Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date())! guard let startDate = Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date()) else { return }
let entries = getData(startDate: startDate, endDate: Date(), includedDays: []) let entries = getData(startDate: startDate, endDate: Date(), includedDays: [])
for entry in entries where Bool.random() { for entry in entries where Bool.random() {

View File

@@ -61,8 +61,6 @@ class ShowBasedOnVoteLogics {
static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool { static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool {
let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData).startOfDay let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData).startOfDay
let endDate = startDate.endOfDay
let entry = DataController.shared.getEntry(byDate: startDate) let entry = DataController.shared.getEntry(byDate: startDate)
return entry == nil || entry?.mood == .missing return entry == nil || entry?.mood == .missing
} }

View File

@@ -176,51 +176,51 @@ struct CreateWidgetView: View {
VStack(alignment: .center) { VStack(alignment: .center) {
Text(String(localized: "create_widget_background_color")) Text(String(localized: "create_widget_background_color"))
ColorPicker("", selection: $customWidget.bgColor) ColorPicker("", selection: $customWidget.bgColor)
.onChange(of: customWidget.mouthColor, perform: { newValue in .onChange(of: customWidget.bgColor) {
EventLogger.log(event: "create_widget_view_update_background_color") EventLogger.log(event: "create_widget_view_update_background_color")
}) }
.labelsHidden() .labelsHidden()
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
VStack(alignment: .center) { VStack(alignment: .center) {
Text(String(localized: "create_widget_inner_color")) Text(String(localized: "create_widget_inner_color"))
ColorPicker("", selection: $customWidget.innerColor) ColorPicker("", selection: $customWidget.innerColor)
.onChange(of: customWidget.mouthColor, perform: { newValue in .onChange(of: customWidget.innerColor) {
EventLogger.log(event: "create_widget_view_update_inner_color") EventLogger.log(event: "create_widget_view_update_inner_color")
}) }
.labelsHidden() .labelsHidden()
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
VStack(alignment: .center) { VStack(alignment: .center) {
Text(String(localized: "create_widget_face_outline_color")) Text(String(localized: "create_widget_face_outline_color"))
ColorPicker("", selection: $customWidget.circleStrokeColor) ColorPicker("", selection: $customWidget.circleStrokeColor)
.onChange(of: customWidget.mouthColor, perform: { newValue in .onChange(of: customWidget.circleStrokeColor) {
EventLogger.log(event: "create_widget_view_update_outline_color") EventLogger.log(event: "create_widget_view_update_outline_color")
}) }
.labelsHidden() .labelsHidden()
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
} }
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(alignment: .center) { VStack(alignment: .center) {
Text(String(localized: "create_widget_view_left_eye_color")) Text(String(localized: "create_widget_view_left_eye_color"))
ColorPicker("", selection: $customWidget.leftEyeColor) ColorPicker("", selection: $customWidget.leftEyeColor)
.onChange(of: customWidget.mouthColor, perform: { newValue in .onChange(of: customWidget.leftEyeColor) {
EventLogger.log(event: "create_widget_view_update_left_eye_color") EventLogger.log(event: "create_widget_view_update_left_eye_color")
}) }
.labelsHidden() .labelsHidden()
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
VStack(alignment: .center) { VStack(alignment: .center) {
Text(String(localized: "create_widget_view_right_eye_color")) Text(String(localized: "create_widget_view_right_eye_color"))
ColorPicker("", selection: $customWidget.rightEyeColor) ColorPicker("", selection: $customWidget.rightEyeColor)
.onChange(of: customWidget.mouthColor, perform: { newValue in .onChange(of: customWidget.rightEyeColor) {
EventLogger.log(event: "create_widget_view_update_right_eye_color") EventLogger.log(event: "create_widget_view_update_right_eye_color")
}) }
.labelsHidden() .labelsHidden()
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
@@ -228,9 +228,9 @@ struct CreateWidgetView: View {
VStack(alignment: .center) { VStack(alignment: .center) {
Text(String(localized: "create_widget_view_mouth_color")) Text(String(localized: "create_widget_view_mouth_color"))
ColorPicker("", selection: $customWidget.mouthColor) ColorPicker("", selection: $customWidget.mouthColor)
.onChange(of: customWidget.mouthColor, perform: { newValue in .onChange(of: customWidget.mouthColor) {
EventLogger.log(event: "create_widget_view_update_mouth_color") EventLogger.log(event: "create_widget_view_update_mouth_color")
}) }
.labelsHidden() .labelsHidden()
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)

View File

@@ -343,7 +343,7 @@ struct TintPickerCompact: View {
ForEach(0..<5, id: \.self) { index in ForEach(0..<5, id: \.self) { index in
ColorPicker("", selection: colorBinding(for: index)) ColorPicker("", selection: colorBinding(for: index))
.labelsHidden() .labelsHidden()
.onChange(of: colorBinding(for: index).wrappedValue) { _ in .onChange(of: colorBinding(for: index).wrappedValue) {
saveCustomMoodTint() saveCustomMoodTint()
} }
} }

View File

@@ -60,39 +60,39 @@ struct TintPickerView: View {
HStack { HStack {
ColorPicker("", selection: $customMoodTint.colorOne) ColorPicker("", selection: $customMoodTint.colorOne)
.onChange(of: customMoodTint.colorOne, perform: { _ in .onChange(of: customMoodTint.colorOne) {
saveCustomMoodTint() saveCustomMoodTint()
}) }
.labelsHidden() .labelsHidden()
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
ColorPicker("", selection: $customMoodTint.colorTwo) ColorPicker("", selection: $customMoodTint.colorTwo)
.labelsHidden() .labelsHidden()
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.onChange(of: customMoodTint.colorTwo, perform: { _ in .onChange(of: customMoodTint.colorTwo) {
saveCustomMoodTint() saveCustomMoodTint()
}) }
ColorPicker("", selection: $customMoodTint.colorThree) ColorPicker("", selection: $customMoodTint.colorThree)
.labelsHidden() .labelsHidden()
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.onChange(of: customMoodTint.colorThree, perform: { _ in .onChange(of: customMoodTint.colorThree) {
saveCustomMoodTint() saveCustomMoodTint()
}) }
ColorPicker("", selection: $customMoodTint.colorFour) ColorPicker("", selection: $customMoodTint.colorFour)
.labelsHidden() .labelsHidden()
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.onChange(of: customMoodTint.colorFour, perform: { _ in .onChange(of: customMoodTint.colorFour) {
saveCustomMoodTint() saveCustomMoodTint()
}) }
ColorPicker("", selection: $customMoodTint.colorFive) ColorPicker("", selection: $customMoodTint.colorFive)
.labelsHidden() .labelsHidden()
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.onChange(of: customMoodTint.colorFive, perform: { _ in .onChange(of: customMoodTint.colorFive) {
saveCustomMoodTint() saveCustomMoodTint()
}) }
} }
.background( .background(
Color.clear Color.clear

View File

@@ -65,7 +65,7 @@ class DayViewViewModel: ObservableObject {
public func update(entry: MoodEntryModel, toMood mood: Mood) { public func update(entry: MoodEntryModel, toMood mood: Mood) {
if !DataController.shared.update(entryDate: entry.forDate, withMood: mood) { if !DataController.shared.update(entryDate: entry.forDate, withMood: mood) {
#warning("show error") print("Failed to update mood entry")
} }
} }

View File

@@ -19,9 +19,7 @@ struct HeaderStatsView : UIViewRepresentable {
init(fakeData: Bool, backDays: Int, moodTint: [Color], textColor: Color) { init(fakeData: Bool, backDays: Int, moodTint: [Color], textColor: Color) {
self.moodTints = moodTint self.moodTints = moodTint
self.textColor = textColor self.textColor = textColor
guard moodTints.count == 5 else { assert(moodTints.count == 5, "mood tint count should be 5")
fatalError("mood tint count dont match")
}
self.tmpHolderToMakeViewDiffefrent = Color.random() self.tmpHolderToMakeViewDiffefrent = Color.random()
entries = [BarChartDataEntry]() entries = [BarChartDataEntry]()
@@ -30,8 +28,10 @@ struct HeaderStatsView : UIViewRepresentable {
if fakeData { if fakeData {
moodEntries = DataController.shared.randomEntries(count: 10) moodEntries = DataController.shared.randomEntries(count: 10)
} else { } else {
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())! guard let daysAgoDate = Calendar.current.date(byAdding: .day, value: -backDays, to: Date()),
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)! let daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgoDate) else {
return
}
moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7])
} }

View File

@@ -990,16 +990,20 @@ class InsightsViewModel: ObservableObject {
var tempStreak = 1 var tempStreak = 1
let today = calendar.startOfDay(for: Date()) let today = calendar.startOfDay(for: Date())
let mostRecent = sortedEntries.first!.forDate guard let mostRecent = sortedEntries.first?.forDate,
if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) { let yesterday = calendar.date(byAdding: .day, value: -1, to: today) else {
return (0, 0)
}
if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: yesterday) {
currentStreak = 1 currentStreak = 1
var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent)! var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent) ?? mostRecent
for entry in sortedEntries.dropFirst() { for entry in sortedEntries.dropFirst() {
let entryDate = entry.forDate let entryDate = entry.forDate
if calendar.isDate(entryDate, inSameDayAs: checkDate) { if calendar.isDate(entryDate, inSameDayAs: checkDate) {
currentStreak += 1 currentStreak += 1
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)! checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
} else { } else {
break break
} }
@@ -1009,7 +1013,7 @@ class InsightsViewModel: ObservableObject {
for i in 1..<sortedEntries.count { for i in 1..<sortedEntries.count {
let currentDate = sortedEntries[i].forDate let currentDate = sortedEntries[i].forDate
let previousDate = sortedEntries[i-1].forDate let previousDate = sortedEntries[i-1].forDate
let dayDiff = calendar.dateComponents([.day], from: currentDate, to: previousDate).day ?? 0 let dayDiff = calendar.dateComponents([.day], from: previousDate, to: currentDate).day ?? 0
if dayDiff == 1 { if dayDiff == 1 {
tempStreak += 1 tempStreak += 1
} else { } else {

View File

@@ -58,29 +58,27 @@ struct MainTabView: View {
}) })
}) })
.onAppear(perform: { .onAppear(perform: {
switch theme { applyTheme(theme)
case .system:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
case .iFeel:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
case .dark:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
case .light:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .light
}
})
.onChange(of: theme, perform: { value in
switch theme {
case .system:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
case .iFeel:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
case .dark:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
case .light:
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .light
}
}) })
.onChange(of: theme) { _, newTheme in
applyTheme(newTheme)
}
}
private func applyTheme(_ theme: Theme) {
let style: UIUserInterfaceStyle
switch theme {
case .system, .iFeel:
style = .unspecified
case .dark:
style = .dark
case .light:
style = .light
}
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else { return }
window.overrideUserInterfaceStyle = style
} }
} }

View File

@@ -112,6 +112,7 @@ struct SettingsView: View {
defer { selectedFile.stopAccessingSecurityScopedResource() } defer { selectedFile.stopAccessingSecurityScopedResource() }
var rows = input.components(separatedBy: "\n") var rows = input.components(separatedBy: "\n")
guard !rows.isEmpty else { return }
rows.removeFirst() rows.removeFirst()
for row in rows { for row in rows {
let stripped = row.replacingOccurrences(of: " +0000", with: "") let stripped = row.replacingOccurrences(of: " +0000", with: "")
@@ -119,10 +120,12 @@ struct SettingsView: View {
if columns.count != 7 { if columns.count != 7 {
continue continue
} }
let forDate = dateFormatter.date(from: columns[3])! guard let forDate = dateFormatter.date(from: columns[3]),
let moodValue = Int(columns[4])! let moodValue = Int(columns[4]) else {
continue
}
let mood = Mood(rawValue: moodValue) ?? .missing let mood = Mood(rawValue: moodValue) ?? .missing
let entryType = EntryType(rawValue: Int(columns[2])!) ?? .listView let entryType = EntryType(rawValue: Int(columns[2]) ?? 0) ?? .listView
DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType) DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType)
} }
@@ -357,7 +360,7 @@ struct SettingsView: View {
Text(String(localized: "settings_use_cloudkit_title")) Text(String(localized: "settings_use_cloudkit_title"))
.foregroundColor(textColor) .foregroundColor(textColor)
}) })
.onChange(of: useCloudKit) { newValue in .onChange(of: useCloudKit) { _, newValue in
EventLogger.log(event: "toggle_use_cloudkit", withData: ["value": newValue]) EventLogger.log(event: "toggle_use_cloudkit", withData: ["value": newValue])
} }
.padding() .padding()
@@ -391,7 +394,7 @@ struct SettingsView: View {
VStack { VStack {
Toggle(String(localized: "settings_use_delete_enable"), Toggle(String(localized: "settings_use_delete_enable"),
isOn: $deleteEnabled) isOn: $deleteEnabled)
.onChange(of: deleteEnabled) { newValue in .onChange(of: deleteEnabled) { _, newValue in
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue]) EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
} }
.foregroundColor(textColor) .foregroundColor(textColor)

View File

@@ -49,7 +49,7 @@ struct AllMoodsTotalTemplate: View, SharingTemplate {
$0.percent > $1.percent $0.percent > $1.percent
}) })
} else { } else {
fatalError("no data") entries = []
} }
} }