From 3eedef2f83b0280c3cc8be160da3485bd78b3964 Mon Sep 17 00:00:00 2001 From: Andreas Grosam Date: Tue, 5 Aug 2025 15:14:05 +0200 Subject: [PATCH] Add a MVVM Example for Comparison --- .../LoadingList-Implementation-Notes.md | 115 ++++++ Documentation/LoadingList-Requirements.md | 213 ++++++++++ .../Complete-Architectural-Pattern-Summary.md | 0 .../State-Encapsulated-Logic-Pattern.md | 0 .../Documentation/ComprehensiveKeyFindings.md | 0 .../Documentation/InputInStatePattern.md | 0 .../KeyFindings-InputInStatePattern.md | 0 .../List/MVVM Sample/LoadingListMVVM.swift | 385 ++++++++++++++++++ 8 files changed, 713 insertions(+) create mode 100644 Documentation/LoadingList-Implementation-Notes.md create mode 100644 Documentation/LoadingList-Requirements.md create mode 100644 Examples/Documentation/Complete-Architectural-Pattern-Summary.md create mode 100644 Examples/Documentation/State-Encapsulated-Logic-Pattern.md create mode 100644 Examples/Sources/Examples/Documentation/ComprehensiveKeyFindings.md create mode 100644 Examples/Sources/Examples/Documentation/InputInStatePattern.md create mode 100644 Examples/Sources/Examples/Documentation/KeyFindings-InputInStatePattern.md create mode 100644 Examples/Sources/Examples/List/MVVM Sample/LoadingListMVVM.swift diff --git a/Documentation/LoadingList-Implementation-Notes.md b/Documentation/LoadingList-Implementation-Notes.md new file mode 100644 index 0000000..9e6e8c0 --- /dev/null +++ b/Documentation/LoadingList-Implementation-Notes.md @@ -0,0 +1,115 @@ +# LoadingList Implementation Notes + +## MVVM Implementation Issues & Solutions + +### Issue: Sheet Presentation Loop + +**Problem Encountered:** +The MVVM implementation experienced a sheet presentation loop where the modal would appear and immediately dismiss repeatedly. + +**Root Causes Identified:** + +1. **Reactive Feedback Loop**: Using computed `Binding` with `@Published` dependencies +2. **Multiple State Sources**: Having both `sheet` model and `isSheetPresented` boolean +3. **Complex Dependencies**: `@Published` properties that depend on each other +4. **ViewModel-Managed Sheet State**: Sheet presentation controlled by ViewModel properties + +**Original Problematic Code:** +```swift +// ViewModel managing sheet state +@Published var presentSheet = false +@Published var sheetTitle = "Load Data" +@Published var sheetInputText = "sample" + +// Complex reactive dependencies +var isSheetPresented: Binding { + Binding( + get: { self.sheet != nil }, + set: { if !$0 { self.sheet = nil } } + ) +} +``` + +**Final Solution Implemented:** +```swift +// View-owned sheet state (no ViewModel involvement) +struct ContentView: View { + @ObservedObject var viewModel: LoadingListViewModel + @State private var showSheet = false // View owns this + @State private var inputText = "sample" // View owns this + + // Simple boolean-based presentation + .sheet(isPresented: $showSheet) { + InputSheetView( + inputText: $inputText, + onCommit: { parameter in + showSheet = false // View controls dismissal + viewModel.startLoading(with: parameter) + }, + onCancel: { + showSheet = false // View controls dismissal + } + ) + } +} +``` + +### Key Solution Principles + +1. **Separation of Concerns**: View owns UI state, ViewModel owns business logic +2. **No ViewModel Sheet Management**: ViewModel doesn't control sheet presentation +3. **Simplified ViewModel**: Only essential `@Published` properties +4. **Direct View State**: `@State` properties for UI-only concerns + +### Comparison with Oak Implementation + +The Oak state machine approach avoids these issues entirely by: +- **Single State Source**: All related state is part of one atomic state value +- **Explicit Transitions**: State changes are explicit and predictable +- **No Reactive Dependencies**: Pure functions prevent feedback loops +- **Declarative Approach**: State is what it is, not computed from other reactive properties + +### Final Working Implementation + +**ViewModel (Simplified):** +```swift +@MainActor +class LoadingListViewModel: ObservableObject { + @Published var data: DataModel? + @Published var isLoading = false + @Published var error: ErrorModel? + + // Computed properties (not @Published) + var isEmpty: Bool { + return data == nil && !isLoading && error == nil + } + + // Simple action methods + func startLoading(with parameter: String) { ... } + func cancelLoading() { ... } + func dismissError() { ... } +} +``` + +**View (Controls Sheet):** +```swift +struct ContentView: View { + @ObservedObject var viewModel: LoadingListViewModel + @State private var showSheet = false + @State private var inputText = "sample" + + // View handles all sheet logic +} +``` + +### Performance Notes + +- **MVVM**: Requires careful separation of View and ViewModel responsibilities +- **Oak**: Single state updates prevent any reactive cycles +- **Testing**: Simplified ViewModel is easier to test than reactive ViewModel state + +## Learning Summary + +The key insight is that **not all UI state should be managed by the ViewModel**. Sheet presentation is a UI concern that can be handled by the View itself, while the ViewModel focuses purely on business logic and data state. + +This demonstrates why Oak's approach is powerful - it forces you to think about state holistically rather than trying to coordinate multiple reactive properties. \ No newline at end of file diff --git a/Documentation/LoadingList-Requirements.md b/Documentation/LoadingList-Requirements.md new file mode 100644 index 0000000..c201292 --- /dev/null +++ b/Documentation/LoadingList-Requirements.md @@ -0,0 +1,213 @@ +# LoadingList Feature Requirements + +**Document Version:** 1.0 +**Date:** August 5, 2025 +**Product Owner:** [Product Owner Name] +**Development Team:** Oak Framework Team +**Epic:** Data Loading and State Management + +## Executive Summary + +This document defines the functional and non-functional requirements for a data loading interface component that demonstrates comprehensive state management patterns for async operations, user input collection, error handling, and data presentation within mobile applications. + +## Business Context + +### Problem Statement +Users require a reliable and intuitive interface for loading dynamic data with proper feedback mechanisms during async operations. The system must handle various failure scenarios gracefully while providing clear user guidance and recovery options. + +### Success Criteria +- Users can successfully initiate data loading operations with custom parameters +- Users receive immediate and clear feedback during loading operations +- Users can recover from error conditions without application restart +- System maintains consistent state throughout all operation phases + +## Functional Requirements + +### FR-001: Application Initialization +**Priority:** Must Have +**Description:** The application shall initialize in an empty state with clear user guidance. + +**Acceptance Criteria:** +- GIVEN the application launches +- WHEN the user views the initial screen +- THEN the system shall display an empty state view +- AND the view shall contain a descriptive message explaining the absence of data +- AND the view shall provide a primary action button labeled "Start" +- AND the message shall read "No data available. Press Start to load items." + +### FR-002: Parameter Input Collection +**Priority:** Must Have +**Description:** The system shall collect user input parameters for data loading operations through a modal interface. + +**Acceptance Criteria:** +- GIVEN the user taps the "Start" button +- WHEN the action is triggered +- THEN the system shall present a modal sheet +- AND the sheet shall contain a text input field +- AND the sheet shall have a descriptive title "Load Data" +- AND the sheet shall include explanatory text "Enter a parameter to load data:" +- AND the sheet shall provide "Cancel" and "Load" action buttons +- AND the "Load" button shall be disabled when input is empty +- AND the input field shall be pre-populated with a default value "sample" + +### FR-003: Loading State Management +**Priority:** Must Have +**Description:** The system shall provide visual feedback during async data loading operations with cancellation capability. + +**Acceptance Criteria:** +- GIVEN the user confirms parameter input +- WHEN the loading operation begins +- THEN the modal sheet shall dismiss +- AND the system shall display a loading overlay +- AND the overlay shall contain a progress indicator +- AND the overlay shall display the message "Loading..." +- AND the overlay shall include descriptive text "Fetching data from service" +- AND the overlay shall provide a "Cancel" button +- AND the cancel button shall terminate the loading operation when pressed + +### FR-004: Successful Data Presentation +**Priority:** Must Have +**Description:** The system shall display loaded data in a structured list format. + +**Acceptance Criteria:** +- GIVEN the data loading operation completes successfully +- WHEN data is received from the service +- THEN the loading overlay shall dismiss +- AND the system shall display the data in a scrollable list +- AND each list item shall show an icon and text content +- AND the list shall support standard iOS list interactions + +### FR-005: Error Handling and Recovery +**Priority:** Must Have +**Description:** The system shall handle service failures gracefully and provide user recovery options. + +**Acceptance Criteria:** +- GIVEN the data loading operation fails +- WHEN a service error occurs +- THEN the loading overlay shall dismiss +- AND the system shall display an error alert +- AND the alert shall have the title "Error" +- AND the alert shall show the error description +- AND the alert shall provide an "OK" button +- AND when "OK" is pressed, the system shall return to an empty state +- AND the empty state shall display "Loading failed" as the title +- AND the empty state shall show the error description +- AND the empty state shall provide a "Try again" button for retry + +### FR-006: Operation Cancellation +**Priority:** Must Have +**Description:** Users shall be able to cancel loading operations and input collection at any time. + +**Acceptance Criteria:** +- GIVEN a loading operation is in progress +- WHEN the user taps the "Cancel" button +- THEN the loading operation shall terminate immediately +- AND the system shall return to the previous content state +- AND no error message shall be displayed for user-initiated cancellation + +**Secondary Scenario:** +- GIVEN the parameter input sheet is displayed +- WHEN the user taps "Cancel" or dismisses the sheet +- THEN the sheet shall close +- AND the system shall return to the empty state +- AND no loading operation shall be initiated + +### FR-007: State Persistence During Modals +**Priority:** Must Have +**Description:** The system shall maintain content state consistency during modal presentations. + +**Acceptance Criteria:** +- GIVEN the system has loaded data successfully +- WHEN a modal (input sheet, loading overlay, or error alert) is presented +- THEN the underlying content shall remain unchanged +- AND when the modal dismisses, the previous content state shall be restored +- AND data shall not be lost during modal interactions + +## Non-Functional Requirements + +### NFR-001: Performance +**Priority:** Must Have +- Loading operations shall provide immediate visual feedback (< 100ms) +- UI state transitions shall be smooth and without perceived delay +- Modal presentations shall animate smoothly using standard iOS animations + +### NFR-002: Reliability +**Priority:** Must Have +- The system shall handle network timeouts gracefully +- The system shall prevent multiple simultaneous loading operations +- The system shall maintain state consistency during all error scenarios +- The system shall not crash due to service failures or invalid responses + +### NFR-003: Usability +**Priority:** Must Have +- Error messages shall be user-friendly and actionable +- Loading states shall clearly indicate system activity +- All interactive elements shall meet iOS accessibility guidelines +- The interface shall follow iOS Human Interface Guidelines + +### NFR-004: Maintainability +**Priority:** Should Have +- State management logic shall be testable in isolation +- UI components shall be reusable across different contexts +- Error handling patterns shall be consistent throughout the application +- Code shall support easy extension for additional loading scenarios + +## Test Scenarios + +### Happy Path +1. Launch application → View empty state +2. Tap "Start" → View input sheet +3. Enter parameter → Tap "Load" +4. View loading overlay → Wait for completion +5. View data list → Verify content display + +### Error Path +1. Launch application → View empty state +2. Tap "Start" → View input sheet +3. Enter parameter → Tap "Load" +4. Service fails → View error alert +5. Tap "OK" → View error recovery state +6. Tap "Try again" → Return to step 2 + +### Cancellation Path +1. Launch application → Initiate loading +2. During loading → Tap "Cancel" +3. Verify return to previous state +4. Verify no error displayed + +## Dependencies + +### Internal Dependencies +- State management system +- UI presentation framework +- Application environment system + +### External Dependencies +- Network connectivity for data service +- Async data service implementation +- Error handling infrastructure + +## Acceptance Definition + +This feature shall be considered complete when: +1. All functional requirements are implemented and tested +2. All non-functional requirements are met +3. Error scenarios are handled according to specifications +4. User experience testing confirms intuitive operation +5. Code review confirms adherence to architectural constraints +6. Integration testing validates service interaction patterns + +## Future Considerations + +### Phase 2 Enhancements +- Pull-to-refresh functionality for data updates +- Offline data caching and synchronization +- Advanced filtering and search capabilities +- Background refresh operations +- Multi-parameter input forms + +### Scalability Considerations +- Pattern reusability for other data loading scenarios +- Template generation for similar workflows +- Performance optimization for large datasets +- Internationalization support for error messages diff --git a/Examples/Documentation/Complete-Architectural-Pattern-Summary.md b/Examples/Documentation/Complete-Architectural-Pattern-Summary.md new file mode 100644 index 0000000..e69de29 diff --git a/Examples/Documentation/State-Encapsulated-Logic-Pattern.md b/Examples/Documentation/State-Encapsulated-Logic-Pattern.md new file mode 100644 index 0000000..e69de29 diff --git a/Examples/Sources/Examples/Documentation/ComprehensiveKeyFindings.md b/Examples/Sources/Examples/Documentation/ComprehensiveKeyFindings.md new file mode 100644 index 0000000..e69de29 diff --git a/Examples/Sources/Examples/Documentation/InputInStatePattern.md b/Examples/Sources/Examples/Documentation/InputInStatePattern.md new file mode 100644 index 0000000..e69de29 diff --git a/Examples/Sources/Examples/Documentation/KeyFindings-InputInStatePattern.md b/Examples/Sources/Examples/Documentation/KeyFindings-InputInStatePattern.md new file mode 100644 index 0000000..e69de29 diff --git a/Examples/Sources/Examples/List/MVVM Sample/LoadingListMVVM.swift b/Examples/Sources/Examples/List/MVVM Sample/LoadingListMVVM.swift new file mode 100644 index 0000000..4dd8cbd --- /dev/null +++ b/Examples/Sources/Examples/List/MVVM Sample/LoadingListMVVM.swift @@ -0,0 +1,385 @@ +import SwiftUI +import Foundation + +// MARK: - Environment Definition +extension EnvironmentValues { + @Entry var dataServiceMVVM: (String) async throws -> LoadingListMVVM.Models.DataModel = { _ in + throw NSError(domain: "DataService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Data service not configured"]) + } +} + +// MARK: - UseCase/Demo "LoadingListMVVM" TOC +enum LoadingListMVVM { + enum ViewModels {} + enum Views {} + enum Models {} +} + +// MARK: - Data Models +extension LoadingListMVVM.Models { + struct DataModel { + let items: [String] + } + + struct EmptyStateModel { + let title: String + let description: String + let actionTitle: String + } + + struct ErrorModel { + let title: String + let message: String + } +} + +// MARK: - Simple ViewModel without reactive cycles +extension LoadingListMVVM.ViewModels { + @MainActor + class LoadingListViewModel: ObservableObject { + // Core state - only what's absolutely necessary + @Published var data: LoadingListMVVM.Models.DataModel? + @Published var isLoading = false + @Published var error: LoadingListMVVM.Models.ErrorModel? + + // Private properties + private var dataService: (String) async throws -> LoadingListMVVM.Models.DataModel = { _ in + throw NSError(domain: "DataService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Data service not configured"]) + } + private var loadingTask: Task? + + // Computed properties (no @Published to avoid cycles) + var isEmpty: Bool { + return data == nil && !isLoading && error == nil + } + + var emptyState: LoadingListMVVM.Models.EmptyStateModel { + if let error = error { + return LoadingListMVVM.Models.EmptyStateModel( + title: "Loading failed", + description: error.message, + actionTitle: "Try again" + ) + } else { + return LoadingListMVVM.Models.EmptyStateModel( + title: "Info", + description: "No data available. Press Start to load items.", + actionTitle: "Start" + ) + } + } + + // MARK: - Initialization + func configure(dataService: @escaping (String) async throws -> LoadingListMVVM.Models.DataModel) { + self.dataService = dataService + } + + // MARK: - User Actions + func startLoading(with parameter: String) { + // Cancel existing loading task + loadingTask?.cancel() + + // Clear previous state + error = nil + + // Start loading + isLoading = true + + loadingTask = Task { @MainActor in + do { + let result = try await self.dataService(parameter) + + // Check if task was cancelled + if Task.isCancelled { + return + } + + // Update UI with success + self.data = result + self.isLoading = false + + } catch { + // Check if task was cancelled + if Task.isCancelled { + return + } + + // Handle error + self.handleError(error) + } + } + } + + func cancelLoading() { + loadingTask?.cancel() + loadingTask = nil + isLoading = false + } + + func dismissError() { + error = nil + } + + func retry() { + // Will be handled by the view + } + + // MARK: - Private Methods + private func handleError(_ error: Error) { + self.error = LoadingListMVVM.Models.ErrorModel( + title: "Error", + message: error.localizedDescription + ) + self.isLoading = false + self.data = nil + } + + // MARK: - Lifecycle + deinit { + loadingTask?.cancel() + } + } +} + +// MARK: - Views with separate sheet management +extension LoadingListMVVM.Views { + + struct MainView: View { + @StateObject private var viewModel = LoadingListMVVM.ViewModels.LoadingListViewModel() + @Environment(\.dataServiceMVVM) private var dataService + + var body: some View { + ContentView(viewModel: viewModel) + .onAppear { + viewModel.configure(dataService: dataService) + } + } + } + + struct ContentView: View { + @ObservedObject var viewModel: LoadingListMVVM.ViewModels.LoadingListViewModel + @State private var showSheet = false + @State private var inputText = "sample" + + var body: some View { + NavigationStack { + ZStack { + // Main content based on state + if viewModel.isEmpty { + EmptyStateView( + model: viewModel.emptyState, + onAction: { + inputText = "sample" + showSheet = true + } + ) + } else if let data = viewModel.data { + DataListView(data: data) + } + + // Loading overlay + if viewModel.isLoading { + LoadingOverlay(onCancel: viewModel.cancelLoading) + } + } + .sheet(isPresented: $showSheet) { + InputSheetView( + inputText: $inputText, + onCommit: { parameter in + showSheet = false + viewModel.startLoading(with: parameter) + }, + onCancel: { + showSheet = false + } + ) + } + .alert( + viewModel.error?.title ?? "Error", + isPresented: .constant(viewModel.error != nil) + ) { + Button("OK") { + viewModel.dismissError() + } + } message: { + Text(viewModel.error?.message ?? "Unknown error") + } + .navigationTitle("Loading List Demo (MVVM)") + } + } + } + + struct EmptyStateView: View { + let model: LoadingListMVVM.Models.EmptyStateModel + let onAction: () -> Void + + var body: some View { + VStack(spacing: 20) { + Image(systemName: "list.bullet.clipboard") + .font(.system(size: 60)) + .foregroundColor(.secondary) + + Text(model.title) + .font(.title2) + .fontWeight(.semibold) + + Text(model.description) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + Button(model.actionTitle) { + onAction() + } + .buttonStyle(.borderedProminent) + } + .padding() + } + } + + struct DataListView: View { + let data: LoadingListMVVM.Models.DataModel + + var body: some View { + List(data.items, id: \.self) { item in + HStack { + Image(systemName: "doc.text") + .foregroundColor(.blue) + Text(item) + } + .padding(.vertical, 4) + } + } + } + + struct LoadingOverlay: View { + let onCancel: () -> Void + + var body: some View { + ZStack { + Color.black.opacity(0.3) + .ignoresSafeArea() + + VStack(spacing: 20) { + ProgressView() + .scaleEffect(1.2) + + Text("Loading...") + .font(.headline) + + Text("Fetching data from service") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + Button("Cancel") { + onCancel() + } + .buttonStyle(.bordered) + } + .padding(30) + .background(.regularMaterial) + .cornerRadius(12) + .shadow(radius: 10) + } + } + } + + struct InputSheetView: View { + @Binding var inputText: String + let onCommit: (String) -> Void + let onCancel: () -> Void + + var body: some View { + NavigationStack { + VStack(spacing: 20) { + Text("Enter a parameter to load data:") + .font(.body) + .multilineTextAlignment(.center) + .padding() + + TextField("Enter parameter", text: $inputText) + .textFieldStyle(.roundedBorder) + .padding(.horizontal) + + Spacer() + } + .navigationTitle("Load Data") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + onCancel() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Load") { + onCommit(inputText) + } + .disabled(inputText.isEmpty) + } + } + } + } + } +} + +// MARK: - Preview Support +extension LoadingListMVVM.Views { + + /// Fake service function suitable for previews and testing + static func previewDataService() -> (String) async throws -> LoadingListMVVM.Models.DataModel { + return { parameter in + // Simulate network delay + try await Task.sleep(nanoseconds: UInt64.random(in: 500_000_000...2_000_000_000)) // 0.5-2 seconds + + // Simulate occasional errors (30% chance) + if Int.random(in: 1...10) <= 3 { + throw NSError( + domain: "PreviewDataService", + code: 500, + userInfo: [NSLocalizedDescriptionKey: "Simulated network error for parameter: \(parameter)"] + ) + } + + // Generate realistic mock data based on parameter + let baseItems = [ + "📄 Document Alpha", + "📊 Report Beta", + "📈 Analysis Gamma", + "📋 Summary Delta", + "🔍 Research Epsilon", + "💡 Insights Zeta", + "📝 Notes Eta", + "🎯 Goals Theta" + ] + + let filteredItems = baseItems.filter { item in + parameter.isEmpty || item.localizedCaseInsensitiveContains(parameter) + } + + let finalItems = filteredItems.isEmpty ? [ + "No results for '\(parameter)'", + "Try a different search term", + "Or browse all available items" + ] : Array(filteredItems.shuffled().prefix(Int.random(in: 2...6))) + + return LoadingListMVVM.Models.DataModel( + items: finalItems + ["Generated at: \(Date().formatted(date: .abbreviated, time: .shortened))"] + ) + } + } +} + +// MARK: - SwiftUI Previews +#Preview("LoadingList MVVM Demo") { + LoadingListMVVM.Views.MainView() + .environment(\.dataServiceMVVM, LoadingListMVVM.Views.previewDataService()) +} + +#Preview("LoadingList MVVM with Error Service") { + LoadingListMVVM.Views.MainView() + .environment(\.dataServiceMVVM) { _ in + try await Task.sleep(nanoseconds: 1_000_000_000) + throw NSError(domain: "Preview", code: 404, userInfo: [NSLocalizedDescriptionKey: "Preview error - service unavailable"]) + } +}