如何在SwiftUI中复用基于Combine构建的Loading状态与ViewModel?
Absolutely! You can reuse your existing LoadingState setup in SwiftUI—you just need to fix how state changes are communicated to SwiftUI's view update system. Here's why your current code isn't working, and two straightforward fixes:
Why your current SwiftUI implementation isn't updating views
SwiftUI relies on ObservableObject's objectWillChange publisher to know when to refresh views. Your custom @Loading property wrapper uses a CurrentValueSubject to emit state changes, but it doesn't trigger the objectWillChange notification on your MyViewModel. That means SwiftUI never gets the signal to re-render when myDataLoadingState updates.
Fix 1: Subscribe to Loading state changes in your ViewModel (no changes to Loading wrapper)
This approach keeps your existing Loading property wrapper intact, and adds a simple subscription in your ViewModel to trigger SwiftUI updates:
public class MyViewModel: ObservableObject { private var cancellables = Set<AnyCancellable>() @Loading<MyData, MyError> var myDataLoadingState = .idle public init() { // Tell SwiftUI to update whenever the Loading state changes $myDataLoadingState .sink { [weak self] _ in self?.objectWillChange.send() } .store(in: &cancellables) } public func getMyData(ID: String) { myDataLoadingState = .loading myDataUseCase.execute(ID: ID) .receive(on: DispatchQueue.main) .sink { completion in guard case .failure(let error) = completion else { return } self.myDataLoadingState = .failure(error) } receiveValue: { myData in self.myDataLoadingState = .success(myData) } .store(in: &cancellables) } }
Fix 2: Modify the Loading wrapper to support ObservableObject updates
If you want a more encapsulated solution (so you don't have to add the subscription to every ViewModel), update your Loading property wrapper to trigger objectWillChange directly:
Step 1: Update the Loading property wrapper
@propertyWrapper public class Loading<T, E: Swift.Error> { public typealias State = LoadingState<T, E> // Weak reference to the host ObservableObject's update publisher public weak var objectWillChange: ObservableObjectPublisher? public var wrappedValue: State { willSet { subject.send(newValue) // Trigger the host ViewModel's update notification objectWillChange?.send() } } public init(wrappedValue: State) { self.wrappedValue = wrappedValue } private lazy var subject = CurrentValueSubject<State, Never>(wrappedValue) public var projectedValue: AnyPublisher<State, Never> { return subject.eraseToAnyPublisher() } }
Step 2: Bind the wrapper to your ViewModel's objectWillChange
public class MyViewModel: ObservableObject { private var cancellables = Set<AnyCancellable>() @Loading<MyData, MyError> var myDataLoadingState = .idle public init() { // Connect the Loading wrapper to the ViewModel's update system _myDataLoadingState.objectWillChange = self.objectWillChange } // Your existing getMyData method stays the same public func getMyData(ID: String) { myDataLoadingState = .loading myDataUseCase.execute(ID: ID) .receive(on: DispatchQueue.main) .sink { completion in guard case .failure(let error) = completion else { return } self.myDataLoadingState = .failure(error) } receiveValue: { myData in self.myDataLoadingState = .success(myData) } .store(in: &cancellables) } }
Bonus: Make LoadingState Equatable (optional but recommended)
While not strictly required for the fixes above, making your LoadingState conform to Equatable adds type safety and aligns with Combine/SwiftUI best practices:
enum LoadingState<T, E: Error>: Equatable where T: Equatable, E: Equatable { case idle case loading case success(T) case failure(E) }
Final Check
Your ContentView code doesn't need any changes! With either fix applied, when myDataLoadingState updates, SwiftUI will receive the objectWillChange notification and re-render the appropriate view based on the new state.
内容的提问来源于stack exchange,提问作者nunzio giulio caggegi

