Files
honeyDueKMP/iosApp/REFACTORING_PLAN.md
Trey t 1e2adf7660 Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project:
- Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations)
- Gradle: rootProject.name, namespace, applicationId
- Android: manifest, strings.xml (all languages), widget resources
- iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig
- iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc.
- Swift source: all class/struct/enum renames
- Deep links: casera:// -> honeydue://, .casera -> .honeydue
- App icons replaced with honeyDue honeycomb icon
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- Database table names preserved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:57 -06:00

35 KiB

iOS Codebase Refactoring Plan

Overview

Refactor the HoneyDue 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: HoneyDueKMM/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

  • ResidenceViewModel - StateFlowObserver + DI
  • TaskViewModel - StateFlowObserver + ActionState + DI
  • ContractorViewModel - StateFlowObserver + DI
  • DocumentViewModel - StateFlowObserver + DI
  • LoginViewModel - StateFlowObserver + DI
  • ProfileViewModel - StateFlowObserver + DI
  • RegisterViewModel - StateFlowObserver + ValidationRules + DI
  • VerifyEmailViewModel - StateFlowObserver + DI
  • PasswordResetViewModel - StateFlowObserver + ValidationRules + DI

Views Updated with ListAsyncContentView

  • ResidencesListView - Now uses ListAsyncContentView with extracted ResidencesContent
  • ContractorsListView - Now uses ListAsyncContentView with extracted ContractorsContent
  • WarrantiesTabContent - Now uses ListAsyncContentView with extracted WarrantiesListContent
  • 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

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

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

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

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:

residenceId: residenceId != nil ? KotlinInt(integerLiteral: Int(residenceId!)) : nil,
isActive: isActive != nil ? KotlinBoolean(bool: isActive!) : nil,

After:

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

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:

@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:

@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

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

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:

@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

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

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

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

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:

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:

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