Harden iOS app: fix concurrency, animations, formatters, privacy, and logging
- Eliminate NumberFormatters shared singleton data race; use local formatters - Add reduceMotion checks to empty-state animations in 3 list views - Wrap 68+ print() statements in #if DEBUG across push notification code - Remove redundant .receive(on: DispatchQueue.main) in SubscriptionCache - Remove redundant initializeLookups() call from iOSApp.init() - Clean up StoreKitManager Task capture in listenForTransactions() - Add memory warning observer to AuthenticatedImage cache - Cache parseContent result in UpgradePromptView init - Add DiskSpace and FileTimestamp API declarations to Privacy Manifest - Add FIXME for analytics debug/production API key separation - Use static formatter in PropertyHeaderCard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ struct ResidencesListView: View {
|
||||
@State private var showingUpgradePrompt = false
|
||||
@State private var showingSettings = false
|
||||
@State private var pushTargetResidenceId: Int32?
|
||||
@State private var navigateToPushResidence = false
|
||||
@State private var showLoginCover = false
|
||||
@StateObject private var authManager = AuthenticationManager.shared
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@@ -100,7 +100,7 @@ struct ResidencesListView: View {
|
||||
UpgradePromptView(triggerKey: "add_second_property", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
.sheet(isPresented: $showingSettings) {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
ProfileTabView()
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,8 @@ struct ResidencesListView: View {
|
||||
viewModel.loadMyResidences()
|
||||
// Also load tasks to populate summary stats
|
||||
taskViewModel.loadTasks()
|
||||
} else {
|
||||
showLoginCover = true
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
@@ -122,9 +124,10 @@ struct ResidencesListView: View {
|
||||
taskViewModel.loadTasks(forceRefresh: true)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $authManager.isAuthenticated.negated) {
|
||||
.fullScreenCover(isPresented: $showLoginCover) {
|
||||
LoginView(onLoginSuccess: {
|
||||
authManager.isAuthenticated = true
|
||||
showLoginCover = false
|
||||
viewModel.loadMyResidences()
|
||||
taskViewModel.loadTasks()
|
||||
})
|
||||
@@ -133,11 +136,13 @@ struct ResidencesListView: View {
|
||||
.onChange(of: authManager.isAuthenticated) { isAuth in
|
||||
if isAuth {
|
||||
// User just logged in or registered - load their residences and tasks
|
||||
showLoginCover = false
|
||||
viewModel.loadMyResidences()
|
||||
taskViewModel.loadTasks()
|
||||
} else {
|
||||
// User logged out - clear data
|
||||
// User logged out - clear data and show login
|
||||
viewModel.myResidences = nil
|
||||
showLoginCover = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToResidence)) { notification in
|
||||
@@ -145,26 +150,18 @@ struct ResidencesListView: View {
|
||||
navigateToResidenceFromPush(residenceId: residenceId)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
NavigationLink(
|
||||
destination: Group {
|
||||
if let residenceId = pushTargetResidenceId {
|
||||
ResidenceDetailView(residenceId: residenceId)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
},
|
||||
isActive: $navigateToPushResidence
|
||||
) {
|
||||
EmptyView()
|
||||
.navigationDestination(isPresented: Binding(
|
||||
get: { pushTargetResidenceId != nil },
|
||||
set: { if !$0 { pushTargetResidenceId = nil } }
|
||||
)) {
|
||||
if let residenceId = pushTargetResidenceId {
|
||||
ResidenceDetailView(residenceId: residenceId)
|
||||
}
|
||||
.hidden()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToResidenceFromPush(residenceId: Int) {
|
||||
pushTargetResidenceId = Int32(residenceId)
|
||||
navigateToPushResidence = true
|
||||
PushNotificationManager.shared.pendingNavigationResidenceId = nil
|
||||
}
|
||||
}
|
||||
@@ -271,6 +268,7 @@ private struct OrganicCardButtonStyle: ButtonStyle {
|
||||
|
||||
private struct OrganicEmptyResidencesView: View {
|
||||
@State private var isAnimating = false
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
@@ -295,7 +293,9 @@ private struct OrganicEmptyResidencesView: View {
|
||||
.frame(width: 160, height: 160)
|
||||
.scaleEffect(isAnimating ? 1.1 : 1.0)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 3).repeatForever(autoreverses: true),
|
||||
isAnimating && !reduceMotion
|
||||
? Animation.easeInOut(duration: 3).repeatForever(autoreverses: true)
|
||||
: .default,
|
||||
value: isAnimating
|
||||
)
|
||||
|
||||
@@ -313,7 +313,9 @@ private struct OrganicEmptyResidencesView: View {
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.offset(y: isAnimating ? -2 : 2)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 2).repeatForever(autoreverses: true),
|
||||
isAnimating && !reduceMotion
|
||||
? Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)
|
||||
: .default,
|
||||
value: isAnimating
|
||||
)
|
||||
}
|
||||
@@ -347,20 +349,14 @@ private struct OrganicEmptyResidencesView: View {
|
||||
.onAppear {
|
||||
isAnimating = true
|
||||
}
|
||||
.onDisappear {
|
||||
isAnimating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
ResidencesListView()
|
||||
}
|
||||
}
|
||||
|
||||
extension Binding where Value == Bool {
|
||||
var negated: Binding<Bool> {
|
||||
Binding<Bool>(
|
||||
get: { !self.wrappedValue },
|
||||
set: { self.wrappedValue = !$0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user