Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI/ Combine Listen to multiple publishers inside an ObservableObject

I have an ObservableObject which inside has two more ObservableObjects. Each of these two ObservableObject has one or more @Published properties inside. My solution works but I'm 100% sure there must be another way. I have to copy/ paste everything in order to make it work. I tried two put those two publishers in the View but then I need to pass a lot of params to the child view. Any ideas how to simplify this?

class PackageService: ObservableObject {
    @Published var package: Package
    @Published var error: Error?
    
    @Published var distance: Double?
    @Published var expectedTravelTime: String?
    
    @Published var amount: Int = 0
    @Published var cost: Double = 0
    
    @Published var annotations = [MKPointAnnotation]()

    @Published var isCalculating = true
    @Published var route: MKRoute?
    
    var amountService = AmountService()
    var routeService = RouteService()
    
    private var cancellables = Set<AnyCancellable>()
    
    init(package: Package) {
        self.package = package
        routeService.calcDirections(source: package.source.toCLLocationCoordinate2D, destination: package.destination.toCLLocationCoordinate2D)
        
        routeService.$route.sink { [self] route in
            self.route = route
            
            amountService.calc(distance: route?.distance)
            calc(route: route)
        }
        .store(in: &cancellables)
        
        routeService.$isCalculating.sink { [self] isCalculating in
            self.isCalculating = isCalculating
        }
        .store(in: &cancellables)
        
        
        routeService.$error.sink { [self] error in
            self.error = error
        }
        .store(in: &cancellables)
        
        amountService.$amount.sink { [self] amount in
            self.amount = amount
        }
        .store(in: &cancellables)
        
        amountService.$cost.sink { [self] cost in
            self.cost = cost
        }
        .store(in: &cancellables)
        
        amountService.$error.sink { [self] error in
            self.error = error
        }
        .store(in: &cancellables)
    }
like image 945
M1X Avatar asked Sep 12 '25 04:09

M1X


1 Answers

Seems like the PackageService is just a thin wrapper around two other services.

It would make sense to use computed properties instead of subscribing and publishing a change for each property:

var isCalculating: Bool { routeService.isCalculating }

var amount: Int { amountService.amount }

// etc...

and to subscribe once in order to signal changes to the view:

routeService.objectWillChange
            .merge(with: amountService.objectWillChange)
            .sink(receiveValue: self.objectWillChange.send)
            .store(in: &cancellables)

Also, consider packaging the various properties in a single data model, e.g.:

struct AmountModel {
   var amount: Int
   var cost: Double
}

and have the AmountService have a single property. This could be a Result<AmountModel, Error> to capture either the data or the error, or, if you need, include the Error as a property.

like image 84
New Dev Avatar answered Sep 13 '25 19:09

New Dev