19

I'm starting to experiment with SwiftUI and I have a situation where I want the latest combination of 5 sliders. I had everything working with 4 sliders, using CombineLatest4, then realized I need another slider, but there's no CombineLatest5.

Any help appreciated.

To clarify the working 4-slider version:

Publishers
    .CombineLatest4($slider1, $slider2, $slider3, $slider4)
    .debounce(for: 0.3, scheduler: DispatchQueue.main)
    .subscribe(subscriber)
Cristik
  • 30,989
  • 25
  • 91
  • 127
jbm
  • 1,248
  • 10
  • 22

5 Answers5

19

CombineLatest2(CombineLatest3, CombineLatest2) should do the trick, shouldn't it?

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Hmm... seems reasonable, but I get `Use of unresolved identifier 'CombineLatest'` when nesting them this way. I'll add my "working" `CombineLatest4()` version to the question, just for clarity. – jbm May 12 '20 at 03:23
  • 3
    How silly these CombineLatestX look... Cannot it take an array? – paulz Jun 09 '21 at 19:17
  • @paulz Yes... https://stackoverflow.com/questions/69774495/chaining-n-requests-in-combine/69774881#69774881 – Daniel T. Nov 12 '21 at 12:25
5

Note - solution extracted from the question


Okay, getting the syntax figured out, @Daniel-t was right. I just needed to create the sub-publishers:

let paramsOne = Publishers
    .CombineLatest3($slider1, $slider2, $slider3)
        
let paramsTwo = Publishers
    .CombineLatest($slider4, $slider5)
        
paramsOne.combineLatest(paramsTwo)
    .debounce(for: 0.3, scheduler: DispatchQueue.main)
    .subscribe(subscriber)

Note that I also had to change what my subscriber expected as input from (Double, Double, Double, Double, Double) to ((Double, Double, Double), (Double, Double)), and that the compiler gave me a misleading and confusing error (something about Schedulers) until I figured out that the input type was wrong.

Cristik
  • 30,989
  • 25
  • 91
  • 127
  • This is by far the best solution without having to add extensions. However do you happen to know how to use naming instead of numbers example" paramsOne.slider1 instead of paramsOne.0 – Tom Nov 18 '22 at 12:18
3

You can use something like:

extension Publisher {
public func combineLatest<P, Q, R, Y>(
    _ publisher1: P,
    _ publisher2: Q,
    _ publisher3: R,
    _ publisher4: Y) ->
    AnyPublisher<(Self.Output, P.Output, Q.Output, R.Output, Y.Output), Self.Failure> where
    P: Publisher,
    Q: Publisher,
    R: Publisher,
    Y: Publisher,
    Self.Failure == P.Failure,
    P.Failure == Q.Failure,
    Q.Failure == R.Failure,
    R.Failure == Y.Failure {
    Publishers.CombineLatest(combineLatest(publisher1, publisher2, publisher3), publisher4).map { tuple, publisher4Value in
        (tuple.0, tuple.1, tuple.2, tuple.3, publisher4Value)
    }.eraseToAnyPublisher()
  }
}

and call it like:

publisher1.combineLatest(publisher2, publisher3, publisher4, publisher5)
m4n0
  • 29,823
  • 27
  • 76
  • 89
3

You can add combineLatest functionality for as many publishers as you need. Below is an example of rather irritating form that will allow sign-in only when all the fields are not empty.

import Combine

final class SignUpVM: ObservableObject {
    @Published var email: String = ""
    @Published var password1: String = ""
    @Published var password2: String = ""
    @Published var password3: String = ""
    @Published var password4: String = ""
    @Published var password5: String = ""
    @Published var password6: String = ""
    
    @Published var isValid = false
    
    private var cancellableSet: Set<AnyCancellable> = []
    
    init() {
        [$email, $password1, $password2, $password3, $password4, $password5, $password6]
            .combineLatest()
            .map { $0.allSatisfy({ !$0.isEmpty })}
            .assign(to: \.isValid, on: self)
            .store(in: &cancellableSet)
    }
    
}

extension Collection where Element: Publisher {
    func combineLatest() -> AnyPublisher<[Element.Output], Element.Failure> {
        var wrapped = map { $0.map { [$0] }.eraseToAnyPublisher() }
        while wrapped.count > 1 {
            wrapped = makeCombinedChunks(input: wrapped)
        }
        return wrapped.first?.eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher()
    }
}

private func makeCombinedChunks<Output, Failure: Swift.Error>(
    input: [AnyPublisher<[Output], Failure>]
) -> [AnyPublisher<[Output], Failure>] {
    sequence(
        state: input.makeIterator(),
        next: { it in it.next().map { ($0, it.next(), it.next(), it.next()) } }
    )
    .map { chunk in
        guard let second = chunk.1 else { return chunk.0 }
        
        guard let third = chunk.2 else {
            return chunk.0
                .combineLatest(second)
                .map { $0.0 + $0.1 }
                .eraseToAnyPublisher()
        }
        
        guard let fourth = chunk.3 else {
            return chunk.0
                .combineLatest(second, third)
                .map { $0.0 + $0.1 + $0.2 }
                .eraseToAnyPublisher()
        }
        
        return chunk.0
            .combineLatest(second, third, fourth)
            .map { $0.0 + $0.1 + $0.2 + $0.3 }
            .eraseToAnyPublisher()
    }
}
kelin
  • 11,323
  • 6
  • 67
  • 104
Paul B
  • 3,989
  • 33
  • 46
1

Expanding on Leonid's answer: if you need support for a transform function, you need something like this:

    public func combineLatest<P, Q, R, Y, T>(
            _ publisher1: P,
            _ publisher2: Q,
            _ publisher3: R,
            _ publisher4: Y,
            _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output, Y.Output) -> T) ->
            AnyPublisher<T, Self.Failure> where
    P: Publisher,
    Q: Publisher,
    R: Publisher,
    Y: Publisher,
    Self.Failure == P.Failure,
    P.Failure == Q.Failure,
    Q.Failure == R.Failure,
    R.Failure == Y.Failure {
        Publishers.CombineLatest(combineLatest(publisher1, publisher2, publisher3), publisher4)
                  .map { tuple, publisher4Value in
                      transform(tuple.0, tuple.1, tuple.2, tuple.3, publisher4Value)
                  }
                  .eraseToAnyPublisher()
    }
ubuntudroid
  • 3,680
  • 6
  • 36
  • 60