Add pull-to-refresh functionality and improve loading states across iOS views

- Added pull-to-refresh to ResidencesListView with force refresh support
- Added pull-to-refresh to AllTasksView with rotating refresh button
- Added pull-to-refresh to ContractorsListView with force refresh
- Added pull-to-refresh to DocumentsTabContent and WarrantiesTabContent
- Improved loading state checks to prevent empty list flash during initial load
- Added refresh button to AllTasksView toolbar with clockwise rotation animation
- Improved UX by disabling refresh button while loading is in progress

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-12 21:11:50 -06:00
parent a61cada072
commit b1a24014ab
9 changed files with 45 additions and 14 deletions

View File

@@ -59,7 +59,7 @@ struct ContractorsListView: View {
} }
// Content // Content
if viewModel.isLoading { if contractors.isEmpty && viewModel.isLoading {
Spacer() Spacer()
ProgressView() ProgressView()
.scaleEffect(1.2) .scaleEffect(1.2)
@@ -95,6 +95,9 @@ struct ContractorsListView: View {
.padding(AppSpacing.md) .padding(AppSpacing.md)
.padding(.bottom, AppSpacing.xxxl) .padding(.bottom, AppSpacing.xxxl)
} }
.refreshable {
loadContractors(forceRefresh: true)
}
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0) Color.clear.frame(height: 0)
} }
@@ -102,7 +105,7 @@ struct ContractorsListView: View {
} }
} }
.navigationTitle("Contractors") .navigationTitle("Contractors")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: AppSpacing.sm) { HStack(spacing: AppSpacing.sm) {
@@ -165,7 +168,7 @@ struct ContractorsListView: View {
} }
} }
private func loadContractors() { private func loadContractors(forceRefresh: Bool = false) {
viewModel.loadContractors( viewModel.loadContractors(
specialty: selectedSpecialty, specialty: selectedSpecialty,
isFavorite: showFavoritesOnly ? true : nil, isFavorite: showFavoritesOnly ? true : nil,

View File

@@ -16,7 +16,7 @@ struct DocumentsTabContent: View {
} }
var body: some View { var body: some View {
if viewModel.isLoading { if filteredDocuments.isEmpty && viewModel.isLoading {
Spacer() Spacer()
ProgressView() ProgressView()
.scaleEffect(1.2) .scaleEffect(1.2)
@@ -46,6 +46,9 @@ struct DocumentsTabContent: View {
.padding(AppSpacing.md) .padding(AppSpacing.md)
.padding(.bottom, AppSpacing.xxxl) .padding(.bottom, AppSpacing.xxxl)
} }
.refreshable {
viewModel.loadDocuments(forceRefresh: true)
}
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0) Color.clear.frame(height: 0)
} }

View File

@@ -18,7 +18,7 @@ struct WarrantiesTabContent: View {
} }
var body: some View { var body: some View {
if viewModel.isLoading { if filteredWarranties.isEmpty && viewModel.isLoading {
Spacer() Spacer()
ProgressView() ProgressView()
.scaleEffect(1.2) .scaleEffect(1.2)
@@ -48,6 +48,9 @@ struct WarrantiesTabContent: View {
.padding(AppSpacing.md) .padding(AppSpacing.md)
.padding(.bottom, AppSpacing.xxxl) .padding(.bottom, AppSpacing.xxxl)
} }
.refreshable {
viewModel.loadDocuments(forceRefresh: true)
}
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0) Color.clear.frame(height: 0)
} }

View File

@@ -93,7 +93,7 @@ struct DocumentsWarrantiesView: View {
} }
} }
.navigationTitle("Documents & Warranties") .navigationTitle("Documents & Warranties")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: AppSpacing.sm) { HStack(spacing: AppSpacing.sm) {

View File

@@ -70,7 +70,7 @@ struct HomeScreenView: View {
} }
} }
.navigationTitle("MyCrib") .navigationTitle("MyCrib")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { Button(action: {

View File

@@ -115,7 +115,7 @@ struct ProfileView: View {
} }
} }
.navigationTitle("Profile") .navigationTitle("Profile")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button("Cancel") {

View File

@@ -105,7 +105,7 @@ struct RegisterView: View {
} }
} }
.navigationTitle("Create Account") .navigationTitle("Create Account")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button("Cancel") {

View File

@@ -11,7 +11,7 @@ struct ResidencesListView: View {
AppColors.background AppColors.background
.ignoresSafeArea() .ignoresSafeArea()
if viewModel.isLoading { if viewModel.myResidences == nil && viewModel.isLoading {
VStack(spacing: AppSpacing.lg) { VStack(spacing: AppSpacing.lg) {
ProgressView() ProgressView()
.scaleEffect(1.2) .scaleEffect(1.2)
@@ -59,6 +59,9 @@ struct ResidencesListView: View {
} }
.padding(.bottom, AppSpacing.xxxl) .padding(.bottom, AppSpacing.xxxl)
} }
.refreshable {
viewModel.loadMyResidences(forceRefresh: true)
}
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0) Color.clear.frame(height: 0)
} }
@@ -66,7 +69,7 @@ struct ResidencesListView: View {
} }
} }
.navigationTitle("My Properties") .navigationTitle("My Properties")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: { Button(action: {

View File

@@ -26,7 +26,7 @@ struct AllTasksView: View {
Color(.systemGroupedBackground) Color(.systemGroupedBackground)
.ignoresSafeArea() .ignoresSafeArea()
if isLoadingTasks { if hasNoTasks && isLoadingTasks {
ProgressView() ProgressView()
} else if let error = tasksError { } else if let error = tasksError {
ErrorView(message: error) { ErrorView(message: error) {
@@ -126,6 +126,9 @@ struct AllTasksView: View {
.padding(.horizontal, 16) .padding(.horizontal, 16)
} }
.scrollTargetBehavior(.viewAligned) .scrollTargetBehavior(.viewAligned)
.refreshable {
loadAllTasks(forceRefresh: true)
}
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0) Color.clear.frame(height: 0)
} }
@@ -144,6 +147,17 @@ struct AllTasksView: View {
} }
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true) .disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
} }
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
loadAllTasks(forceRefresh: true)
}) {
Image(systemName: "arrow.clockwise")
.rotationEffect(.degrees(isLoadingTasks ? 360 : 0))
.animation(isLoadingTasks ? .linear(duration: 1).repeatForever(autoreverses: false) : .default, value: isLoadingTasks)
}
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true || isLoadingTasks)
}
} }
.sheet(isPresented: $showAddTask) { .sheet(isPresented: $showAddTask) {
AddTaskWithResidenceView( AddTaskWithResidenceView(
@@ -172,13 +186,18 @@ struct AllTasksView: View {
loadAllTasks() loadAllTasks()
} }
} }
.onChange(of: taskViewModel.isLoading) { isLoading in
if !isLoading {
loadAllTasks()
}
}
.onAppear { .onAppear {
loadAllTasks() loadAllTasks()
residenceViewModel.loadMyResidences() residenceViewModel.loadMyResidences()
} }
} }
private func loadAllTasks() { private func loadAllTasks(forceRefresh: Bool = false) {
guard TokenStorage.shared.getToken() != nil else { return } guard TokenStorage.shared.getToken() != nil else { return }
isLoadingTasks = true isLoadingTasks = true
@@ -186,7 +205,7 @@ struct AllTasksView: View {
Task { Task {
do { do {
let result = try await APILayer.shared.getTasks(forceRefresh: false) let result = try await APILayer.shared.getTasks(forceRefresh: forceRefresh)
await MainActor.run { await MainActor.run {
if let success = result as? ApiResultSuccess<TaskColumnsResponse> { if let success = result as? ApiResultSuccess<TaskColumnsResponse> {
self.tasksResponse = success.data self.tasksResponse = success.data