Files
honeyDueKMP/iosApp/iosApp/Core/LoadingOverlay.swift
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

174 lines
4.6 KiB
Swift

import SwiftUI
// MARK: - Loading Overlay View Modifier
/// A view modifier that displays a loading overlay on top of the content
struct LoadingOverlay: ViewModifier {
let isLoading: Bool
let message: String?
func body(content: Content) -> some View {
ZStack {
content
.disabled(isLoading)
.blur(radius: isLoading ? 1 : 0)
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)
.multilineTextAlignment(.center)
}
}
.padding(24)
.background(Color.black.opacity(0.7))
.cornerRadius(12)
}
}
.animation(.easeInOut(duration: 0.2), value: isLoading)
}
}
// MARK: - View Extension
extension View {
/// Apply a loading overlay to the view
/// - Parameters:
/// - isLoading: Whether to show the loading overlay
/// - message: Optional message to display below the spinner
func loadingOverlay(isLoading: Bool, message: String? = nil) -> some View {
modifier(LoadingOverlay(isLoading: isLoading, message: message))
}
}
// MARK: - Inline Loading View
/// A simple inline loading indicator
struct InlineLoadingView: View {
let message: String?
init(message: String? = nil) {
self.message = message
}
var body: some View {
HStack(spacing: 8) {
ProgressView()
if let message = message {
Text(message)
.foregroundColor(Color.appTextSecondary)
.font(.subheadline)
}
}
}
}
// MARK: - Button Loading State
/// A view modifier that shows loading state on a button
struct ButtonLoadingModifier: ViewModifier {
let isLoading: Bool
func body(content: Content) -> some View {
content
.opacity(isLoading ? 0 : 1)
.overlay {
if isLoading {
ProgressView()
.tint(.white)
}
}
.disabled(isLoading)
}
}
extension View {
/// Apply loading state to a button
func buttonLoading(_ isLoading: Bool) -> some View {
modifier(ButtonLoadingModifier(isLoading: isLoading))
}
}
// MARK: - Shimmer Loading Effect
/// A shimmer effect for loading placeholders
struct ShimmerModifier: ViewModifier {
@State private var phase: CGFloat = 0
func body(content: Content) -> some View {
content
.overlay(
GeometryReader { geometry in
LinearGradient(
gradient: Gradient(colors: [
Color.clear,
Color.white.opacity(0.4),
Color.clear
]),
startPoint: .leading,
endPoint: .trailing
)
.frame(width: geometry.size.width * 2)
.offset(x: -geometry.size.width + phase * geometry.size.width * 2)
}
)
.mask(content)
.onAppear {
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
phase = 1
}
}
}
}
extension View {
/// Apply shimmer loading effect
func shimmer() -> some View {
modifier(ShimmerModifier())
}
}
// MARK: - Skeleton Loading View
/// A skeleton placeholder view for loading states
struct SkeletonView: View {
let width: CGFloat?
let height: CGFloat
init(width: CGFloat? = nil, height: CGFloat = 16) {
self.width = width
self.height = height
}
var body: some View {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray.opacity(0.3))
.frame(width: width, height: height)
.shimmer()
}
}
// MARK: - Preview
#if DEBUG
struct LoadingOverlay_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text("Content behind loading overlay")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.loadingOverlay(isLoading: true, message: "Loading...")
}
}
#endif