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>
174 lines
4.6 KiB
Swift
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
|