c5f2bee83f
- 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>
146 lines
5.6 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|