From e2a9981d4f19f8f61a6e768f6c6b0632faa7a507 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 17:44:35 +0000 Subject: [PATCH] feat: Implement Inspirational Message Feature Adds a feature to display a new inspirational message to you each time you open the application. Key changes: - Added `InspirationalMessages.swift` with 80 predefined quotes. - Integrated `UserDefaultsClient` into the TCA `AppFeature` environment using `@Dependency` for managing the index of the last displayed message. - Updated `AppFeature` (State, Action, Reducer) to handle the logic for: - Loading the last displayed message index from UserDefaults (defaulting to -1 on first launch). - Calculating the next message index in a sequential cycle. - Storing the new index to UserDefaults. - Updating the state with the message to be displayed. - Modified `AppView.swift` to: - Trigger the loading of the inspirational message on view appearance. - Display the current message at the top of the main screen using a SwiftUI `Text` view with appropriate styling. - The feature follows the technical requirements TR-IM-001 to TR-IM-005, ensuring messages are stored correctly, cycled sequentially, integrated with TCA, displayed in the UI, and handle the initial launch state. --- FastPath/AppFeature.swift | 60 ++++++++++++++ FastPath/Models/InspirationalMessages.swift | 87 +++++++++++++++++++++ FastPath/Views/AppView.swift | 51 ++++++++---- 3 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 FastPath/Models/InspirationalMessages.swift diff --git a/FastPath/AppFeature.swift b/FastPath/AppFeature.swift index 91968f0..9ae1125 100644 --- a/FastPath/AppFeature.swift +++ b/FastPath/AppFeature.swift @@ -7,17 +7,58 @@ import SwiftUI import ComposableArchitecture +import Foundation // Ensure Foundation is imported + +struct UserDefaultsClient { + var integerForKey: @Sendable (String) -> Int? // Make @Sendable for actor safety + var setInteger: @Sendable (Int, String) async -> Void // Make async if it could involve async operations, though UserDefaults is sync +} + +extension UserDefaultsClient: DependencyKey { + static let liveValue = Self( + integerForKey: { key in + // UserDefaults.integer(forKey:) returns 0 if key DNE or is not an Int. + // To correctly return nil if the key genuinely doesn't exist, + // we check object(forKey:) first. + if UserDefaults.standard.object(forKey: key) == nil { + return nil + } + return UserDefaults.standard.integer(forKey: key) + }, + setInteger: { value, key in + UserDefaults.standard.set(value, forKey: key) + } + ) + + // Add a testValue if you plan to write tests for this + static let testValue = Self( + integerForKey: { _ in nil }, // Default test implementation + setInteger: { _, _ in } + ) +} + +extension DependencyValues { + var userDefaultsClient: UserDefaultsClient { + get { self[UserDefaultsClient.self] } + set { self[UserDefaultsClient.self] = newValue } + } +} @Reducer struct AppFeature { + @Dependency(\.userDefaultsClient) var userDefaultsClient + struct State: Equatable { var fasting = FastingFeature.State() var path = StackState() + var currentInspirationalMessage: String? = nil } enum Action: Equatable { case fasting(FastingFeature.Action) case path(StackAction) + case loadInspirationalMessage + case inspirationalMessageResponse(String) } @Reducer @@ -47,6 +88,25 @@ struct AppFeature { case .fasting(.showHistory): state.path.append(.history(state.fasting)) return .none + + case .loadInspirationalMessage: + let lastDisplayedMessageIndex = userDefaultsClient.integerForKey("lastDisplayedMessageIndex") ?? -1 + + guard !Quotes.allMessages.isEmpty else { + return .send(.inspirationalMessageResponse("No messages available.")) + } + + let nextIndex = (lastDisplayedMessageIndex + 1) % Quotes.allMessages.count + let message = Quotes.allMessages[nextIndex] + + return .run { send in + await userDefaultsClient.setInteger(nextIndex, "lastDisplayedMessageIndex") + await send(.inspirationalMessageResponse(message)) + } + + case .inspirationalMessageResponse(let message): + state.currentInspirationalMessage = message + return .none case .fasting: return .none diff --git a/FastPath/Models/InspirationalMessages.swift b/FastPath/Models/InspirationalMessages.swift new file mode 100644 index 0000000..98744a1 --- /dev/null +++ b/FastPath/Models/InspirationalMessages.swift @@ -0,0 +1,87 @@ +// InspirationalMessages.swift +import Foundation + +struct Quotes { + static let allMessages: [String] = [ + "Every fast is a step towards a healthier you.", + "Discipline today, results tomorrow.", + "Your body is capable of amazing things.", + "Stay focused on your goals.", + "Small changes lead to big results.", + "Believe in your strength to succeed.", + "Patience and persistence are key.", + "You are stronger than you think.", + "Embrace the journey, trust the process.", + "Today's effort is tomorrow's reward.", + "One day at a time, one fast at a time.", + "The best project you'll ever work on is you.", + "Nourish your body, cherish your progress.", + "Celebrate every small victory.", + "Consistency is what transforms average into excellence.", + "Focus on progress, not perfection.", + "You have the power to make a change.", + "Let your determination light your way.", + "Every moment is a fresh beginning.", + "Be proud of how far you've come.", + "The journey of a thousand miles begins with a single step.", + "Keep pushing, even when it's tough.", + "Your health is an investment, not an expense.", + "Unlock your potential, one fast at a time.", + "Make yourself a priority.", + "The only bad fast is the one not started.", + "Success is the sum of small efforts, repeated.", + "Challenge yourself, you might surprise yourself.", + "Listen to your body, it knows what it needs.", + "Transform your mind, transform your body.", + "It's not about being perfect, it's about effort.", + "Find strength in your discipline.", + "Every completed fast builds momentum.", + "Fuel your body, respect your temple.", + "Small steps every day add up to big changes.", + "The power of choice is your greatest tool.", + "Stay committed to your decisions.", + "Progress is progress, no matter how small.", + "You are creating a healthier future.", + "Believe in the process, trust your journey.", + "Let go of what was, embrace what will be.", + "Your dedication is inspiring.", + "Keep your eyes on the prize.", + "The discipline of today shapes the success of tomorrow.", + "You are resilient and capable.", + "Embrace the challenge, enjoy the results.", + "Every fast is a testament to your willpower.", + "Stay positive, stay strong.", + "Your journey is unique and valid.", + "Invest in yourself, you're worth it.", + "The path to health is paved with consistency.", + "Find joy in the process of becoming stronger.", + "Your commitment is your greatest asset.", + "Keep going, your future self will thank you.", + "Master your habits, master your life.", + "Each fast is a new opportunity for growth.", + "Strength doesn't come from what you can do, it comes from overcoming things you once thought you couldn't.", + "Be patient with yourself, great things take time.", + "The best view comes after the hardest climb.", + "Focus on your 'why' to stay motivated.", + "Turn 'I can't' into 'I can'.", + "Your body achieves what the mind believes.", + "Don't wait for opportunity, create it.", + "Every choice you make is a step towards your goal.", + "Be stronger than your excuses.", + "The discipline you build now will serve you for life.", + "You are in control of your choices.", + "Let your progress be your motivation.", + "Small, consistent efforts yield powerful results.", + "Trust in your ability to persevere.", + "Every sunrise brings a new chance to shine.", + "Dedication sees you through the tough times.", + "You are building a better version of yourself.", + "Celebrate your efforts, not just the outcomes.", + "The reward of discipline is a healthier, happier you.", + "Keep your spirit strong and your goals in sight.", + "Embrace the power of now.", + "Your journey, your rules, your success.", + "Stay true to your path and purpose.", + "You've got this!" + ] +} diff --git a/FastPath/Views/AppView.swift b/FastPath/Views/AppView.swift index 4d07db2..a035081 100644 --- a/FastPath/Views/AppView.swift +++ b/FastPath/Views/AppView.swift @@ -12,23 +12,40 @@ struct AppView: View { let store: StoreOf var body: some View { - NavigationStackStore( - store.scope(state: \.path, action: { .path($0) }) - ) { - FastingView( - store: store.scope( - state: \.fasting, - action: { .fasting($0) } - ) - ) - } destination: { state in - switch state { - case .history: - CaseLet( - /AppFeature.Path.State.history, - action: AppFeature.Path.Action.history, - then: HistoryView.init(store:) - ) + WithViewStore(self.store, observe: { $0 }) { viewStore in // Observe the whole state or specific parts + VStack { // Wrap existing content in a VStack + // Display the inspirational message + if let message = viewStore.currentInspirationalMessage, !message.isEmpty { + Text(message) + .padding() // Add some padding for better appearance + .font(.caption) // Style as per TR-IM-004 (e.g., smaller font) + .foregroundColor(.gray) // Subtle color + .multilineTextAlignment(.center) // Center if it's long + } + + // Existing NavigationStackStore + NavigationStackStore( + self.store.scope(state: \.path, action: { .path($0) }) + ) { + FastingView( + store: self.store.scope( + state: \.fasting, + action: { .fasting($0) } + ) + ) + } destination: { state in + switch state { + case .history: + CaseLet( + /AppFeature.Path.State.history, + action: AppFeature.Path.Action.history, + then: HistoryView.init(store:) + ) + } + } + } + .onAppear { + viewStore.send(.loadInspirationalMessage) } } }