# iOS Codebase Refactoring Plan ## Overview Refactor the MyCrib iOS codebase to improve adherence to SOLID principles and DRY patterns. This plan targets ~1,500 lines of code reduction while significantly improving testability and maintainability. **Scope**: `MyCribKMM/iosApp/iosApp/` **Estimated Effort**: 4-5 focused sessions **Risk Level**: Medium (ViewModels are central to app functionality) ### User Preferences - **DI Approach**: Factory pattern (`Dependencies.current.make*()`) - **View Refactoring**: Yes, include Views alongside ViewModels - **File Organization**: New `Core/` folder for foundational abstractions --- ## Completion Status | Session | Status | Notes | |---------|--------|-------| | Session 1: Core Abstractions | ✅ Complete | StateFlowObserver, ValidationRules created; BaseViewModel skipped per user preference | | Session 2: Type Conversions | ✅ Complete | KotlinTypeExtensions.swift created with .asKotlin helpers | | Session 3: State Management | ✅ Complete | ActionState.swift created for TaskViewModel | | Session 4: Dependency Injection | ✅ Complete | Dependencies.swift + TokenStorageProtocol (simplified due to Kotlin API constraints) | | Session 5: View Layer | ✅ Complete | ViewState, LoadingOverlay, AsyncContentView, Form State containers created | ### Implementation Notes 1. **BaseViewModel Skipped**: Per user preference, ViewModels continue to extend `ObservableObject` directly 2. **Protocol Simplification**: Full ViewModel protocols removed due to Kotlin-generated API parameter label mismatches; only `TokenStorageProtocol` retained 3. **Form State Containers**: Created as standalone structs that can be adopted incrementally by views 4. **All ViewModels Updated**: 9 ViewModels now use StateFlowObserver pattern and support optional DI ### Files Created ``` iosApp/Core/ ├── StateFlowObserver.swift ✅ ├── ValidationRules.swift ✅ ├── ActionState.swift ✅ ├── ViewState.swift ✅ ├── LoadingOverlay.swift ✅ ├── AsyncContentView.swift ✅ ├── Dependencies.swift ✅ ├── Protocols/ │ └── ViewModelProtocols.swift ✅ (TokenStorageProtocol only) ├── FormStates/ │ ├── TaskFormStates.swift ✅ │ ├── ResidenceFormState.swift ✅ │ ├── ContractorFormState.swift ✅ │ └── DocumentFormState.swift ✅ └── Extensions/ └── KotlinTypeExtensions.swift ✅ ``` ### ViewModels Updated - [x] ResidenceViewModel - StateFlowObserver + DI - [x] TaskViewModel - StateFlowObserver + ActionState + DI - [x] ContractorViewModel - StateFlowObserver + DI - [x] DocumentViewModel - StateFlowObserver + DI - [x] LoginViewModel - StateFlowObserver + DI - [x] ProfileViewModel - StateFlowObserver + DI - [x] RegisterViewModel - StateFlowObserver + ValidationRules + DI - [x] VerifyEmailViewModel - StateFlowObserver + DI - [x] PasswordResetViewModel - StateFlowObserver + ValidationRules + DI ### Views Updated with ListAsyncContentView - [x] ResidencesListView - Now uses `ListAsyncContentView` with extracted `ResidencesContent` - [x] ContractorsListView - Now uses `ListAsyncContentView` with extracted `ContractorsContent` - [x] WarrantiesTabContent - Now uses `ListAsyncContentView` with extracted `WarrantiesListContent` - [x] DocumentsTabContent - Now uses `ListAsyncContentView` with extracted `DocumentsListContent` --- ## Phase 1: Core Abstractions ### 1.1 Create StateFlow Observation Utility **Problem**: 26 instances of identical StateFlow observation boilerplate across all ViewModels (~800 lines) **Solution**: Create a reusable utility that handles Kotlin StateFlow observation with proper MainActor handling. **New File**: `iosApp/Core/StateFlowObserver.swift` ```swift import Foundation import ComposeApp @MainActor class StateFlowObserver { /// Observe a Kotlin StateFlow and handle loading/success/error states static func observe( _ stateFlow: Kotlinx_coroutines_coreStateFlow, onLoading: (() -> Void)? = nil, onSuccess: @escaping (T) -> Void, onError: ((String) -> Void)? = nil, resetState: (() -> Void)? = nil ) { Task { for await state in stateFlow { if state is ApiResultLoading { await MainActor.run { onLoading?() } } else if let success = state as? ApiResultSuccess { await MainActor.run { onSuccess(success.data!) } resetState?() break } else if let error = state as? ApiResultError { await MainActor.run { let message = ErrorMessageParser.parse(error.message ?? "Unknown error") onError?(message) } resetState?() break } else if state is ApiResultIdle { // Idle state, continue observing continue } } } } /// Observe with automatic isLoading/errorMessage binding static func observeWithState( _ stateFlow: Kotlinx_coroutines_coreStateFlow, isLoading: Binding, errorMessage: Binding, onSuccess: @escaping (T) -> Void, resetState: (() -> Void)? = nil ) { observe( stateFlow, onLoading: { isLoading.wrappedValue = true }, onSuccess: { data in isLoading.wrappedValue = false onSuccess(data) }, onError: { error in isLoading.wrappedValue = false errorMessage.wrappedValue = error }, resetState: resetState ) } } ``` **Files to Update** (remove duplicated observation code): - `Residence/ResidenceViewModel.swift` - `Task/TaskViewModel.swift` - `Contractor/ContractorViewModel.swift` - `Documents/DocumentViewModel.swift` - `Login/LoginViewModel.swift` - `Profile/ProfileViewModel.swift` - `Register/RegisterView.swift` - `VerifyEmail/VerifyEmailViewModel.swift` - `PasswordReset/PasswordResetViewModel.swift` --- ### 1.2 Create Base ViewModel Class **Problem**: Every ViewModel repeats `isLoading`, `errorMessage`, and basic state handling patterns. **Solution**: Abstract common ViewModel functionality into a base class. **New File**: `iosApp/Core/BaseViewModel.swift` ```swift import Foundation import SwiftUI import ComposeApp @MainActor class BaseViewModel: ObservableObject { @Published var isLoading: Bool = false @Published var errorMessage: String? /// Clear error message func clearError() { errorMessage = nil } /// Set loading state func setLoading(_ loading: Bool) { isLoading = loading } /// Handle error with optional parsing func handleError(_ message: String?) { errorMessage = ErrorMessageParser.parse(message ?? "An unexpected error occurred") isLoading = false } /// Observe StateFlow with automatic state binding func observe( _ stateFlow: Kotlinx_coroutines_coreStateFlow, onSuccess: @escaping (T) -> Void, resetState: (() -> Void)? = nil ) { StateFlowObserver.observe( stateFlow, onLoading: { [weak self] in self?.isLoading = true }, onSuccess: { [weak self] (data: T) in self?.isLoading = false onSuccess(data) }, onError: { [weak self] error in self?.handleError(error) }, resetState: resetState ) } } ``` **ViewModels to Refactor** (inherit from BaseViewModel): - All 10 ViewModels will extend `BaseViewModel` instead of `ObservableObject` --- ### 1.3 Create Validation Rules Module **Problem**: Email, password, and field validation logic duplicated across 4+ ViewModels. **Solution**: Centralize all validation rules in a single module. **New File**: `iosApp/Core/ValidationRules.swift` ```swift import Foundation enum ValidationError: LocalizedError { case required(field: String) case invalidEmail case passwordTooShort(minLength: Int) case passwordMismatch case invalidCode(expectedLength: Int) case invalidUsername case custom(message: String) var errorDescription: String? { switch self { case .required(let field): return "\(field) is required" case .invalidEmail: return "Please enter a valid email address" case .passwordTooShort(let minLength): return "Password must be at least \(minLength) characters" case .passwordMismatch: return "Passwords do not match" case .invalidCode(let length): return "Code must be \(length) digits" case .invalidUsername: return "Username can only contain letters, numbers, and underscores" case .custom(let message): return message } } } struct ValidationRules { // MARK: - Email Validation static func validateEmail(_ email: String) -> ValidationError? { let trimmed = email.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return .required(field: "Email") } let emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" let predicate = NSPredicate(format: "SELF MATCHES %@", emailRegex) if !predicate.evaluate(with: trimmed) { return .invalidEmail } return nil } // MARK: - Password Validation static func validatePassword(_ password: String, minLength: Int = 8) -> ValidationError? { if password.isEmpty { return .required(field: "Password") } if password.count < minLength { return .passwordTooShort(minLength: minLength) } return nil } static func validatePasswordMatch(_ password: String, _ confirmPassword: String) -> ValidationError? { if password != confirmPassword { return .passwordMismatch } return nil } // MARK: - Code Validation static func validateCode(_ code: String, expectedLength: Int = 6) -> ValidationError? { let trimmed = code.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return .required(field: "Code") } if trimmed.count != expectedLength || !trimmed.allSatisfy({ $0.isNumber }) { return .invalidCode(expectedLength: expectedLength) } return nil } // MARK: - Username Validation static func validateUsername(_ username: String) -> ValidationError? { let trimmed = username.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return .required(field: "Username") } let usernameRegex = "^[A-Za-z0-9_]+$" let predicate = NSPredicate(format: "SELF MATCHES %@", usernameRegex) if !predicate.evaluate(with: trimmed) { return .invalidUsername } return nil } // MARK: - Required Field static func validateRequired(_ value: String, fieldName: String) -> ValidationError? { if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return .required(field: fieldName) } return nil } } ``` **Files to Update** (remove inline validation): - `Login/LoginViewModel.swift` - `Register/RegisterView.swift` - `PasswordReset/PasswordResetViewModel.swift` - `Profile/ProfileViewModel.swift` --- ## Phase 2: Type Conversion & Extensions ### 2.1 Create Kotlin Type Conversion Extensions **Problem**: Verbose type conversions scattered across ViewModels (~150 lines of boilerplate). **Solution**: Create clean extensions for Swift-to-Kotlin type conversions. **New File**: `iosApp/Extensions/KotlinTypeExtensions.swift` ```swift import Foundation import ComposeApp // MARK: - Optional Bool to KotlinBoolean extension Optional where Wrapped == Bool { var asKotlin: KotlinBoolean? { self.map { KotlinBoolean(bool: $0) } } } extension Bool { var asKotlin: KotlinBoolean { KotlinBoolean(bool: self) } } // MARK: - Optional Int to KotlinInt extension Optional where Wrapped == Int { var asKotlin: KotlinInt? { self.map { KotlinInt(integerLiteral: $0) } } } extension Optional where Wrapped == Int32 { var asKotlin: KotlinInt? { self.map { KotlinInt(integerLiteral: Int($0)) } } } extension Int { var asKotlin: KotlinInt { KotlinInt(integerLiteral: self) } } extension Int32 { var asKotlin: KotlinInt { KotlinInt(integerLiteral: Int(self)) } } // MARK: - Optional Double to KotlinDouble extension Optional where Wrapped == Double { var asKotlin: KotlinDouble? { self.map { KotlinDouble(double: $0) } } } extension Double { var asKotlin: KotlinDouble { KotlinDouble(double: self) } } // MARK: - String to Optional Double (for form inputs) extension String { var asOptionalDouble: Double? { Double(self) } var asKotlinDouble: KotlinDouble? { Double(self).map { KotlinDouble(double: $0) } } } // MARK: - Optional Long to KotlinLong extension Optional where Wrapped == Int64 { var asKotlin: KotlinLong? { self.map { KotlinLong(longLong: $0) } } } extension Int64 { var asKotlin: KotlinLong { KotlinLong(longLong: self) } } ``` **Files to Update** (simplify type conversions): - `Documents/DocumentViewModel.swift` - `Contractor/ContractorViewModel.swift` - `Task/TaskViewModel.swift` - `Residence/ResidenceViewModel.swift` **Before**: ```swift residenceId: residenceId != nil ? KotlinInt(integerLiteral: Int(residenceId!)) : nil, isActive: isActive != nil ? KotlinBoolean(bool: isActive!) : nil, ``` **After**: ```swift residenceId: residenceId.asKotlin, isActive: isActive.asKotlin, ``` --- ## Phase 3: State Management Improvements ### 3.1 Create Action State Pattern **Problem**: `TaskViewModel` has 7 separate boolean flags for action states, which is error-prone. **Solution**: Replace with a single enum-based state. **New File**: `iosApp/Core/ActionState.swift` ```swift import Foundation /// Generic action state for tracking async operations enum ActionState: Equatable where T: Equatable { case idle case inProgress case success(T) case failure(String) var isInProgress: Bool { if case .inProgress = self { return true } return false } var isSuccess: Bool { if case .success = self { return true } return false } var successValue: T? { if case .success(let value) = self { return value } return nil } var errorMessage: String? { if case .failure(let message) = self { return message } return nil } static func == (lhs: ActionState, rhs: ActionState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): return true case (.inProgress, .inProgress): return true case (.success(let l), .success(let r)): return l == r case (.failure(let l), .failure(let r)): return l == r default: return false } } } /// Specialized for void actions (create, delete, etc.) typealias VoidActionState = ActionState extension ActionState where T == Void { static var success: ActionState { .success(()) } } /// Task-specific action types enum TaskActionType: Equatable { case created case updated case cancelled case uncancelled case markedInProgress case archived case unarchived case completed } ``` **File to Update**: `Task/TaskViewModel.swift` **Before**: ```swift @Published var taskCreated: Bool = false @Published var taskUpdated: Bool = false @Published var taskCancelled: Bool = false @Published var taskUncancelled: Bool = false @Published var taskMarkedInProgress: Bool = false @Published var taskArchived: Bool = false @Published var taskUnarchived: Bool = false ``` **After**: ```swift @Published var lastAction: ActionState = .idle // Usage in views: .onChange(of: viewModel.lastAction) { newValue in if case .success(let action) = newValue { switch action { case .created: // handle created case .cancelled: // handle cancelled // etc. } viewModel.lastAction = .idle // reset } } ``` --- ## Phase 4: Dependency Injection Foundation ### 4.1 Create Protocol Abstractions for Kotlin ViewModels **Problem**: Hard-wired Kotlin ViewModel dependencies make unit testing impossible. **Solution**: Create protocols that abstract the Kotlin layer, allowing mock implementations. **New File**: `iosApp/Protocols/ViewModelProtocols.swift` ```swift import Foundation import ComposeApp // MARK: - Residence ViewModel Protocol protocol ResidenceViewModelProtocol { var myResidencesState: Kotlinx_coroutines_coreStateFlow { get } var createState: Kotlinx_coroutines_coreStateFlow { get } var updateState: Kotlinx_coroutines_coreStateFlow { get } var deleteState: Kotlinx_coroutines_coreStateFlow { get } func loadMyResidences(forceRefresh: Bool) func createResidence(request: ResidenceCreateRequest) func updateResidence(id: Int32, request: ResidenceUpdateRequest) func deleteResidence(id: Int32) func resetCreateState() func resetUpdateState() func resetDeleteState() } // Make Kotlin ViewModel conform extension ComposeApp.ResidenceViewModel: ResidenceViewModelProtocol {} // MARK: - Task ViewModel Protocol protocol TaskViewModelProtocol { var kanbanState: Kotlinx_coroutines_coreStateFlow { get } var createState: Kotlinx_coroutines_coreStateFlow { get } var updateState: Kotlinx_coroutines_coreStateFlow { get } func loadKanbanBoard(residenceId: Int32, forceRefresh: Bool) func createTask(request: TaskCreateRequest) func updateTask(id: Int32, request: TaskUpdateRequest) func cancelTask(id: Int32) func markTaskInProgress(id: Int32) // ... other methods } extension ComposeApp.TaskViewModel: TaskViewModelProtocol {} // MARK: - Auth ViewModel Protocol protocol AuthViewModelProtocol { var loginState: Kotlinx_coroutines_coreStateFlow { get } var logoutState: Kotlinx_coroutines_coreStateFlow { get } func login(username: String, password: String) func logout() func resetLoginState() } extension ComposeApp.AuthViewModel: AuthViewModelProtocol {} ``` ### 4.2 Create Simple Dependency Container **New File**: `iosApp/Core/Dependencies.swift` ```swift import Foundation import ComposeApp /// Simple dependency container for ViewModels @MainActor class Dependencies { static let shared = Dependencies() // MARK: - Kotlin ViewModel Factories func makeResidenceViewModel() -> ResidenceViewModelProtocol { ComposeApp.ResidenceViewModel() } func makeTaskViewModel() -> TaskViewModelProtocol { ComposeApp.TaskViewModel() } func makeAuthViewModel() -> AuthViewModelProtocol { ComposeApp.AuthViewModel() } func makeContractorViewModel() -> ComposeApp.ContractorViewModel { ComposeApp.ContractorViewModel() } func makeDocumentViewModel() -> ComposeApp.DocumentViewModel { ComposeApp.DocumentViewModel() } func makeProfileViewModel() -> ComposeApp.ProfileViewModel { ComposeApp.ProfileViewModel() } // MARK: - For Testing #if DEBUG static var testInstance: Dependencies? static var current: Dependencies { testInstance ?? shared } #else static var current: Dependencies { shared } #endif } // MARK: - Environment Key private struct DependenciesKey: EnvironmentKey { static let defaultValue = Dependencies.shared } extension EnvironmentValues { var dependencies: Dependencies { get { self[DependenciesKey.self] } set { self[DependenciesKey.self] = newValue } } } ``` **Updated ViewModel Pattern**: ```swift @MainActor class ResidenceViewModel: BaseViewModel { @Published var myResidences: MyResidencesResponse? private let sharedViewModel: ResidenceViewModelProtocol init(sharedViewModel: ResidenceViewModelProtocol? = nil) { self.sharedViewModel = sharedViewModel ?? Dependencies.current.makeResidenceViewModel() super.init() } func loadMyResidences(forceRefresh: Bool = false) { sharedViewModel.loadMyResidences(forceRefresh: forceRefresh) observe(sharedViewModel.myResidencesState) { [weak self] (response: MyResidencesResponse) in self?.myResidences = response } resetState: { [weak self] in // Reset if needed } } } ``` --- ## Phase 5: Multi-Step Flow Pattern (Optional Enhancement) ### 5.1 Create Multi-Step Flow Protocol **Problem**: `PasswordResetViewModel` (315 lines) manages complex multi-step flow inline. **Solution**: Extract reusable multi-step flow pattern. **New File**: `iosApp/Core/MultiStepFlow.swift` ```swift import Foundation import SwiftUI protocol MultiStepFlow: ObservableObject { associatedtype Step: Hashable & CaseIterable var currentStep: Step { get set } var canGoBack: Bool { get } var canGoForward: Bool { get } func goToNextStep() func goToPreviousStep() func reset() } extension MultiStepFlow where Step: CaseIterable, Step.AllCases: BidirectionalCollection { var canGoBack: Bool { currentStep != Step.allCases.first } var canGoForward: Bool { currentStep != Step.allCases.last } func goToNextStep() { let allCases = Array(Step.allCases) guard let currentIndex = allCases.firstIndex(of: currentStep), currentIndex < allCases.count - 1 else { return } currentStep = allCases[currentIndex + 1] } func goToPreviousStep() { let allCases = Array(Step.allCases) guard let currentIndex = allCases.firstIndex(of: currentStep), currentIndex > 0 else { return } currentStep = allCases[currentIndex - 1] } func reset() { currentStep = Step.allCases.first! } } ``` --- ## Phase 6: View Layer Refactoring ### 6.1 Create Reusable View Components **Problem**: Views have heavy state management and duplicated patterns for loading, error handling, and form state. **Solution**: Create composable view builders and state containers. **New File**: `iosApp/Core/ViewState.swift` ```swift import Foundation import SwiftUI /// Represents the state of async data loading enum ViewState { case idle case loading case loaded(T) case error(String) var isLoading: Bool { if case .loading = self { return true } return false } var data: T? { if case .loaded(let data) = self { return data } return nil } var errorMessage: String? { if case .error(let message) = self { return message } return nil } } /// Container for form field state struct FormField { var value: T var error: String? var isDirty: Bool = false mutating func validate(_ validator: (T) -> ValidationError?) { if let validationError = validator(value) { error = validationError.localizedDescription } else { error = nil } } mutating func touch() { isDirty = true } } extension FormField where T == String { init() { self.value = "" self.error = nil self.isDirty = false } } ``` ### 6.2 Create Loading State View Modifier **New File**: `iosApp/Core/LoadingOverlay.swift` ```swift import SwiftUI struct LoadingOverlay: ViewModifier { let isLoading: Bool let message: String? func body(content: Content) -> some View { ZStack { content .disabled(isLoading) if isLoading { Color.black.opacity(0.3) .ignoresSafeArea() VStack(spacing: 12) { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(1.2) if let message = message { Text(message) .foregroundColor(.white) .font(.subheadline) } } .padding(24) .background(Color.black.opacity(0.7)) .cornerRadius(12) } } } } extension View { func loadingOverlay(isLoading: Bool, message: String? = nil) -> some View { modifier(LoadingOverlay(isLoading: isLoading, message: message)) } } ``` ### 6.3 Create Async Content View **New File**: `iosApp/Core/AsyncContentView.swift` ```swift import SwiftUI /// Reusable view for handling async content states struct AsyncContentView: View { let state: ViewState let content: (T) -> Content let loading: () -> Loading let error: (String, @escaping () -> Void) -> Error let onRetry: () -> Void init( state: ViewState, @ViewBuilder content: @escaping (T) -> Content, @ViewBuilder loading: @escaping () -> Loading = { ProgressView() }, @ViewBuilder error: @escaping (String, @escaping () -> Void) -> Error, onRetry: @escaping () -> Void ) { self.state = state self.content = content self.loading = loading self.error = error self.onRetry = onRetry } var body: some View { switch state { case .idle: EmptyView() case .loading: loading() case .loaded(let data): content(data) case .error(let message): error(message, onRetry) } } } // Convenience initializer with default error view extension AsyncContentView where Error == DefaultErrorView { init( state: ViewState, @ViewBuilder content: @escaping (T) -> Content, @ViewBuilder loading: @escaping () -> Loading = { ProgressView() }, onRetry: @escaping () -> Void ) { self.state = state self.content = content self.loading = loading self.error = { message, retry in DefaultErrorView(message: message, onRetry: retry) } self.onRetry = onRetry } } struct DefaultErrorView: View { let message: String let onRetry: () -> Void var body: some View { VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.red) Text(message) .multilineTextAlignment(.center) .foregroundColor(.secondary) Button("Retry", action: onRetry) .buttonStyle(.bordered) } .padding() } } ``` ### 6.4 Refactor Form Views Pattern **Problem**: Form views like `CompleteTaskView` have 12+ `@State` properties. **Solution**: Use form state containers. **Example Refactoring** (CompleteTaskView): **Before**: ```swift struct CompleteTaskView: View { @State private var completedByName: String = "" @State private var actualCost: String = "" @State private var notes: String = "" @State private var rating: Int = 3 @State private var selectedItems: [PhotosPickerItem] = [] @State private var selectedImages: [UIImage] = [] @State private var completionDate: Date = Date() @State private var showingImagePicker: Bool = false @State private var isSubmitting: Bool = false @State private var errorMessage: String? // ... more state } ``` **After**: ```swift struct CompleteTaskFormState { var completedByName = FormField() var actualCost = FormField() var notes = FormField() var rating: Int = 3 var completionDate: Date = Date() var selectedImages: [UIImage] = [] var isValid: Bool { completedByName.error == nil && actualCost.error == nil } mutating func validateAll() { completedByName.validate { ValidationRules.validateRequired($0, fieldName: "Completed By") } } } struct CompleteTaskView: View { @State private var formState = CompleteTaskFormState() @StateObject private var viewModel: TaskViewModel // Reduced from 12+ to 2 state properties } ``` ### 6.5 Views to Refactor | View File | Current State Props | Target | Changes | |-----------|-------------------|--------|---------| | `Task/CompleteTaskView.swift` | 12+ | 2-3 | Extract form state | | `Task/AddTaskView.swift` | 10+ | 2-3 | Extract form state | | `Task/EditTaskView.swift` | 10+ | 2-3 | Extract form state | | `Residence/AddResidenceView.swift` | 8+ | 2-3 | Extract form state | | `Residence/EditResidenceView.swift` | 8+ | 2-3 | Extract form state | | `Contractor/AddContractorView.swift` | 6+ | 2-3 | Extract form state | | `Documents/AddDocumentView.swift` | 8+ | 2-3 | Extract form state | | `Login/LoginView.swift` | 5 | 2 | Use AsyncContentView | | `Residence/ResidencesListView.swift` | 5 | 2 | Use AsyncContentView | --- ## Implementation Order ### Session 1: Core Abstractions (Highest Impact) 1. **Create Core/ folder and new files**: - `iosApp/Core/StateFlowObserver.swift` - `iosApp/Core/BaseViewModel.swift` - `iosApp/Core/ValidationRules.swift` 2. **Refactor ViewModels to use StateFlowObserver**: - Start with `ResidenceViewModel` as the template - Apply pattern to remaining 9 ViewModels 3. **Migrate ViewModels to inherit from BaseViewModel** ### Session 2: Type Conversions & Extensions 1. **Create new file**: - `iosApp/Core/Extensions/KotlinTypeExtensions.swift` 2. **Update ViewModels to use new extensions**: - `DocumentViewModel` (heaviest user) - `ContractorViewModel` - `TaskViewModel` - `ResidenceViewModel` ### Session 3: State Management 1. **Create new file**: - `iosApp/Core/ActionState.swift` 2. **Refactor TaskViewModel**: - Replace 7 boolean flags with `ActionState` - Update all views that observe these flags 3. **Apply pattern to other ViewModels** (ContractorViewModel, DocumentViewModel) ### Session 4: Dependency Injection 1. **Create new files**: - `iosApp/Core/Protocols/ViewModelProtocols.swift` - `iosApp/Core/Dependencies.swift` 2. **Update ViewModels to accept protocol-based dependencies** 3. **Verify testability** with sample mock ### Session 5: View Layer Refactoring 1. **Create new files**: - `iosApp/Core/ViewState.swift` - `iosApp/Core/LoadingOverlay.swift` - `iosApp/Core/AsyncContentView.swift` 2. **Create form state structs** for each form view: - `CompleteTaskFormState` - `AddTaskFormState` - `AddResidenceFormState` - etc. 3. **Refactor form views** to use new state containers 4. **Update list views** to use `AsyncContentView` pattern --- ## Files Summary ### New Files to Create (11 files in Core/) | File | Purpose | Priority | |------|---------|----------| | `Core/StateFlowObserver.swift` | Reusable StateFlow observation | P0 | | `Core/BaseViewModel.swift` | Common ViewModel base class | P0 | | `Core/ValidationRules.swift` | Centralized validation | P0 | | `Core/Extensions/KotlinTypeExtensions.swift` | Type conversion helpers | P1 | | `Core/ActionState.swift` | Action state enum | P1 | | `Core/Protocols/ViewModelProtocols.swift` | Kotlin ViewModel abstractions | P2 | | `Core/Dependencies.swift` | Simple DI container (factory pattern) | P2 | | `Core/MultiStepFlow.swift` | Multi-step flow protocol | P3 | | `Core/ViewState.swift` | View state enum + FormField | P2 | | `Core/LoadingOverlay.swift` | Loading overlay modifier | P2 | | `Core/AsyncContentView.swift` | Async content wrapper view | P2 | ### Files to Modify (10 ViewModels) | File | Changes | Impact | |------|---------|--------| | `Residence/ResidenceViewModel.swift` | Inherit BaseViewModel, use StateFlowObserver | High | | `Task/TaskViewModel.swift` | Replace boolean flags, use ActionState | High | | `Contractor/ContractorViewModel.swift` | Use StateFlowObserver, type extensions | Medium | | `Documents/DocumentViewModel.swift` | Heavy type conversion cleanup | High | | `Login/LoginViewModel.swift` | Extract validation, use base class | Medium | | `Profile/ProfileViewModel.swift` | Minor cleanup | Low | | `Register/RegisterView.swift` | Extract validation | Low | | `VerifyEmail/VerifyEmailViewModel.swift` | Use StateFlowObserver | Low | | `PasswordReset/PasswordResetViewModel.swift` | Validation, possibly MultiStepFlow | Medium | ### Views to Modify (9 form/list views) | File | Changes | Impact | |------|---------|--------| | `Task/CompleteTaskView.swift` | Extract form state | High | | `Task/AddTaskView.swift` | Extract form state | High | | `Task/EditTaskView.swift` | Extract form state | Medium | | `Residence/AddResidenceView.swift` | Extract form state | Medium | | `Residence/EditResidenceView.swift` | Extract form state | Medium | | `Residence/ResidencesListView.swift` | Use AsyncContentView | Medium | | `Contractor/AddContractorView.swift` | Extract form state | Low | | `Documents/AddDocumentView.swift` | Extract form state | Low | | `Login/LoginView.swift` | Use AsyncContentView | Low | --- ## Expected Outcomes | Metric | Before | After | Improvement | |--------|--------|-------|-------------| | Total Lines | ~13,837 | ~11,800 | -15% | | Duplicated Patterns | 26+ | 0 | -100% | | ViewModel Boilerplate | ~200 lines each | ~80 lines each | -60% | | View State Properties | 8-12 per form | 2-3 per form | -75% | | Type Conversion Lines | ~150 | ~30 | -80% | | Unit Test Coverage | 0% | Possible | Enabled | | Validation Sources | 4+ files | 1 file | Centralized | --- ## Risk Mitigation 1. **Incremental Approach**: Each phase is independently deployable 2. **Template First**: Refactor `ResidenceViewModel` completely as template before others 3. **Testing**: Run app after each ViewModel refactor to catch regressions 4. **Git Strategy**: Commit after each major refactor step 5. **View Testing**: Test each form after state extraction to ensure bindings work --- ## Final Core/ Folder Structure ``` iosApp/Core/ ├── StateFlowObserver.swift # Kotlin StateFlow observation utility ├── BaseViewModel.swift # Base class for all ViewModels ├── ValidationRules.swift # Centralized validation logic ├── ActionState.swift # Generic action state enum ├── ViewState.swift # View state + FormField ├── LoadingOverlay.swift # Loading overlay modifier ├── AsyncContentView.swift # Async content wrapper ├── MultiStepFlow.swift # Multi-step flow protocol ├── Dependencies.swift # Simple factory-based DI ├── Extensions/ │ └── KotlinTypeExtensions.swift └── Protocols/ └── ViewModelProtocols.swift ```