Files
honeyDueKMP/iosApp/iosApp/Core/StateFlowObserver.swift
T
Trey t c5f2bee83f 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>
2026-03-04 23:15:42 -06:00

146 lines
5.6 KiB
Swift

import Foundation
import ComposeApp
/// Utility for observing Kotlin StateFlow and handling ApiResult states.
/// This eliminates the repeated boilerplate pattern across ViewModels.
///
/// **Important:** All observation methods return a `Task` that the caller MUST store
/// and cancel when the observer is no longer needed (e.g., in `deinit` or when the
/// view disappears). Failing to store the returned `Task` will cause a memory leak
/// because the async `for await` loop retains its closure indefinitely.
///
/// Example usage:
/// ```swift
/// private var observationTask: Task<Void, Never>?
///
/// func startObserving() {
/// observationTask = StateFlowObserver.observe(stateFlow, onSuccess: { ... })
/// }
///
/// deinit { observationTask?.cancel() }
/// ```
@MainActor
enum StateFlowObserver {
/// Observe a Kotlin StateFlow and handle loading/success/error states.
///
/// - Returns: A `Task` that the caller **must** store and cancel when observation
/// is no longer needed. Discarding this task leaks the observation loop.
/// - Parameters:
/// - stateFlow: The Kotlin StateFlow to observe
/// - onLoading: Called when state is ApiResultLoading
/// - onSuccess: Called when state is ApiResultSuccess with the data
/// - onError: Called when state is ApiResultError with parsed message
/// - resetState: Optional closure to reset the StateFlow state after handling
static func observe<T>(
_ stateFlow: SkieSwiftStateFlow<ApiResult<T>>,
onLoading: (() -> Void)? = nil,
onSuccess: @escaping (T) -> Void,
onError: ((String) -> Void)? = nil,
resetState: (() -> Void)? = nil
) -> Task<Void, Never> {
let task = 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 {
if let data = success.data {
onSuccess(data)
}
}
resetState?()
break
} else if let error = ApiResultBridge.error(from: state) {
await MainActor.run {
let message = ErrorMessageParser.parse(error.message)
onError?(message)
}
resetState?()
break
} else if state is ApiResultIdle {
// Idle state, continue observing
continue
}
}
}
return task
}
/// Observe a StateFlow with automatic isLoading and errorMessage binding.
/// Use this when you want standard loading/error state management.
///
/// - Returns: A `Task` that the caller **must** store and cancel when observation
/// is no longer needed. Discarding this task leaks the observation loop.
/// - Parameters:
/// - stateFlow: The Kotlin StateFlow to observe
/// - loadingSetter: Closure to set loading state
/// - errorSetter: Closure to set error message
/// - onSuccess: Called when state is ApiResultSuccess with the data
/// - resetState: Optional closure to reset the StateFlow state
static func observeWithState<T>(
_ stateFlow: SkieSwiftStateFlow<ApiResult<T>>,
loadingSetter: @escaping (Bool) -> Void,
errorSetter: @escaping (String?) -> Void,
onSuccess: @escaping (T) -> Void,
resetState: (() -> Void)? = nil
) -> Task<Void, Never> {
return observe(
stateFlow,
onLoading: {
loadingSetter(true)
},
onSuccess: { data in
loadingSetter(false)
onSuccess(data)
},
onError: { error in
loadingSetter(false)
errorSetter(error)
},
resetState: resetState
)
}
/// Observe a StateFlow with a completion callback.
/// Use this for create/update/delete operations that need success/failure feedback.
///
/// - Returns: A `Task` that the caller **must** store and cancel when observation
/// is no longer needed. Discarding this task leaks the observation loop.
/// - Parameters:
/// - stateFlow: The Kotlin StateFlow to observe
/// - loadingSetter: Closure to set loading state
/// - errorSetter: Closure to set error message
/// - onSuccess: Called when state is ApiResultSuccess with the data
/// - completion: Called with true on success, false on error
/// - resetState: Optional closure to reset the StateFlow state
static func observeWithCompletion<T>(
_ stateFlow: SkieSwiftStateFlow<ApiResult<T>>,
loadingSetter: @escaping (Bool) -> Void,
errorSetter: @escaping (String?) -> Void,
onSuccess: ((T) -> Void)? = nil,
completion: @escaping (Bool) -> Void,
resetState: (() -> Void)? = nil
) -> Task<Void, Never> {
return observe(
stateFlow,
onLoading: {
loadingSetter(true)
},
onSuccess: { data in
loadingSetter(false)
onSuccess?(data)
completion(true)
},
onError: { error in
loadingSetter(false)
errorSetter(error)
completion(false)
},
resetState: resetState
)
}
}