2

I would like to update some variables with values received when handling a newly published value. For example, given:

class ProductViewModel: ObservableObject {
    @Published var PublishedX: Int = 0
    @Published var PublishedY: Int = 0
    @Published var PublishedProduct: Int = 0
    // ...
    init() {
        productPublisher = Publishers.CombineLatest(external.XPublisher, internal.YPublisher)
            // .assignAndContinue(\.PublishedX, \.PublishedY) // something like this
            .flatMap(MyPublishers.secretMultiplication)
            .assign(to: \.PublishedProduct, on: self)
     }
 }

I would like to also assign the new values of XPublisher and YPublisher to variables (PublishedX and PublishedY respectively).

Is there a way to set these two variables and then continue handling the event?

chustar
  • 12,225
  • 24
  • 81
  • 119
  • By the time I call `sink` (after the map) I would only have the multiplied value, not the original two X and Y values. – chustar Mar 30 '20 at 17:37
  • I also changed the map to a flatMap to help show the problem – chustar Mar 30 '20 at 17:42
  • 1
    Oh, I see, I thought the problem was the `.assign` line, but it's the `.assignAndContinue` line. :) – matt Mar 30 '20 at 19:40
  • 1
    So you could actually do the assignment as part of the `.flatMap`, or you could have another `.flatMap` that produces a Just or a `.map` that produces `$0`. In other words, you use some sort of map to perform arbitrary code and keep going. But... – matt Mar 30 '20 at 19:42
  • 2
    I think the Combine Way is to call `.share()` so that you have two subscribers and just add a `.sink` that assigns to the two variables. – matt Mar 30 '20 at 19:43
  • @matt I was avoiding the `.map()` option in case `.assign()` does something special with `@Published` vars. Thanks, I'll take a look at `.share()`. – chustar Mar 30 '20 at 20:17

2 Answers2

2

UPDATE

If your deployment target is a 2020 system (like iOS 14+ or macOS 11+), you can use a different version of the assign operator to avoid retain cycles and to avoid storing cancellables:

init() {
    external.XPublisher.assign(to: $PublishedX)
    external.YPublisher.assign(to: $PublishedY)
    external.XPublisher
        .combineLatest(external.YPublisher) { $0 * $1 }
        .assign(to: $PublishedProduct)
}

ORIGINAL

There is no built-in variant of assign(to:on:) that returns another Publisher instead of a Cancellable.

Just use multiple assigns:

class ProductViewModel: ObservableObject {
    @Published var PublishedX: Int = 0
    @Published var PublishedY: Int = 0
    @Published var PublishedProduct: Int = 0

    init() {
        external.XPublisher
            .assign(to: \.PublishedX, on: self)
            .store(in: &tickets)
        internal.YPublisher
            .assign(to: \.PublishedY, on: self)
            .store(in: &tickets)
        external.XPublisher
            .combineLatest(internal.YPublisher) { $0 * $1 }
            .assign(to: \.PublishedProduct, on: self)
            .store(in: &tickets)
    }

    private var tickets: [AnyCancellable] = []
}

Note that these subscriptions create retain cycles. Swift will not be able to destroy an instance of ProductViewModel until the tickets array is cleared. (This is not a property of my suggestion. Your original code also needs to store its subscription somewhere, else it will be cancelled immediately.)

Also, the existence of PublishedProduct is questionable. Why not just a computed property?

var product: Int { PublishedX * PublishedY }
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thank you. I thought any active subscriptions would be cancelled when the object was destroyed, not the other way around? Can you attach multiple subscribers to the same publisher? I thought any new subscriber would cancel any existing subscriptions? – chustar Mar 30 '20 at 20:32
  • 1
    The `AnyCancellable` return by `assign(to:on:)` represents the subscription and must be stored somewhere, because when it is destroyed, it cancels the subscription. Under the covers, an `assign` subscription must hold a reference to the target of the assignment (in this case, the `ProductViewModel` object), and so storing the `AnyCancellable` in a property of the `ProductViewModel` object creates a retain cycle between the `ProductViewModel` and the subscription. – rob mayoff Mar 30 '20 at 20:36
  • Every `Publisher` in Apple's `Combine` framework supports multiple simultaneous subscriptions. – rob mayoff Mar 30 '20 at 20:37
  • @robmayoff "Every Publisher in Apple's Combine framework supports multiple simultaneous subscriptions" I'm sure you're aware that that's an oversimplification. You won't get thrown off by another subscription coming along, but you won't necessarily get the result you might expect from the word "simultaneous" unless you also say `share()` as I suggested in my comment. – matt Mar 30 '20 at 21:22
  • Yes, I'm aware. But as far as I know, using `share` only really matters if your publisher has side effects. I didn't think it was relevant to this question. – rob mayoff Mar 30 '20 at 23:31
1

You may want to check out this library

From their ReadMe:

var label1: UILabel
var label2: UILabel
var text: UITextField

["hey", "there", "friend"]
    .publisher
    .assign(to: \.text, on: label1,
            and: \.text, on: label2,
            and: \.text, on: text)
Mohamed Mo Kawsara
  • 4,400
  • 2
  • 27
  • 43