SwiftUI/ Комбинируйте Прослушивание нескольких издателей внутри наблюдаемого объекта

#ios #swift #xcode #swiftui #combine

Вопрос:

У меня есть ObservableObject , в котором внутри есть еще два ObservableObjects . У каждого из этих двух ObservableObject есть одно или несколько @Published свойств внутри. Мое решение работает, но я на 100% уверен, что должен быть другой способ. Я должен скопировать/ вставить все, чтобы заставить его работать. Я попытался поместить этих двух издателей в представление, но затем мне нужно передать много параметров в дочернее представление. Есть идеи, как это упростить?

 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: amp;cancellables)
        
        routeService.$isCalculating.sink { [self] isCalculating in
            self.isCalculating = isCalculating
        }
        .store(in: amp;cancellables)
        
        
        routeService.$error.sink { [self] error in
            self.error = error
        }
        .store(in: amp;cancellables)
        
        amountService.$amount.sink { [self] amount in
            self.amount = amount
        }
        .store(in: amp;cancellables)
        
        amountService.$cost.sink { [self] cost in
            self.cost = cost
        }
        .store(in: amp;cancellables)
        
        amountService.$error.sink { [self] error in
            self.error = error
        }
        .store(in: amp;cancellables)
    }
 

Ответ №1:

Похоже PackageService , что это всего лишь тонкая оболочка вокруг двух других сервисов.

Имело бы смысл использовать вычисляемые свойства вместо подписки и публикации изменений для каждого свойства:

 var isCalculating: Bool { routeService.isCalculating }

var amount: Int { amountService.amount }

// etc...
 

и подписаться один раз, чтобы сообщить об изменениях в представлении:

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

Кроме того, рассмотрите возможность упаковки различных свойств в единую модель данных, например:

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

и у AmountService них есть одно свойство. Это может быть Result<AmountModel, Error> для захвата данных или ошибки, или, если вам нужно, включите свойство Error как свойство.

Комментарии:

1. Отличный ответ, я знал, что его можно упростить.

2. но как я реагирую на изменения внутри routeService , так как при обновлении свойства внутри routeService мне нужно запустить amountService.calc(distance: route?.distance)

3. @M1X, если вам нужно выполнить какое-то действие, не связанное со SwiftUI, то да, вы могли бы наблюдать с помощью Combine, например route.$distance.sink … но вы спрашивали о наблюдении в SwiftUI. Все, что нужно SwiftUI (это также отвечает на вопрос вашего второго комментария), — это знать, что что-то изменилось (см. .sink(receiveValue: self.objectWillChange.send) В моем ответе), а затем он перезагружает представление. Итак, вы могли бы сделать .onChange(packageSvc.distance) { packageSvc.recalcAmount() }

4. Хорошо, я думаю, что понял, что это .sink(receiveValue: self.objectWillChange.send) уведомляет о представлении. .onChange(packageSvc.distance) { packageSvc.recalcAmount() } чтобы уловить изменения в представлении. И если я хочу выполнить какое-то действие, которое мне нужно использовать sink .

5. Также странно, но вычисляемое свойство, которое возвращает a @Published , будет отражать изменения в представлении даже без .sink(receiveValue: self.objectWillChange.send)