Files
honeyDueKMP/iosApp/REFACTORING_PLAN.md
Trey t 67e0057bfa Refactor iOS codebase with SOLID/DRY patterns
Core Infrastructure:
- Add StateFlowObserver for reusable Kotlin StateFlow observation
- Add ValidationRules for centralized form validation
- Add ActionState enum for tracking async operations
- Add KotlinTypeExtensions with .asKotlin helpers
- Add Dependencies factory for dependency injection
- Add ViewState, FormField, and FormState for view layer
- Add LoadingOverlay and AsyncContentView components
- Add form state containers (Task, Residence, Contractor, Document)

ViewModel Updates (9 files):
- Refactor all ViewModels to use StateFlowObserver pattern
- Add optional DI support via initializer parameters
- Reduce boilerplate by ~330 lines across ViewModels

View Updates (4 files):
- Update ResidencesListView to use ListAsyncContentView
- Update ContractorsListView to use ListAsyncContentView
- Update WarrantiesTabContent to use ListAsyncContentView
- Update DocumentsTabContent to use ListAsyncContentView

Net reduction: -332 lines (1007 removed, 675 added)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 21:15:11 -06:00

1214 lines
35 KiB
Markdown

# 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<T>(
_ 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<T> {
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<T>(
_ stateFlow: Kotlinx_coroutines_coreStateFlow,
isLoading: Binding<Bool>,
errorMessage: Binding<String?>,
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<T>(
_ 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<T>: 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<T>, rhs: ActionState<T>) -> 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<Void>
extension ActionState where T == Void {
static var success: ActionState<Void> { .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<TaskActionType> = .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<T> {
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<T> {
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<T, Content: View, Loading: View, Error: View>: View {
let state: ViewState<T>
let content: (T) -> Content
let loading: () -> Loading
let error: (String, @escaping () -> Void) -> Error
let onRetry: () -> Void
init(
state: ViewState<T>,
@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<T>,
@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<String>()
var actualCost = FormField<String>()
var notes = FormField<String>()
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<TaskActionType>`
- 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
```