SwiftUI and CombineLatest with more than 4 values
Asked Answered
T

5

21

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)
Timbuktu answered 12/5, 2020 at 1:18 Comment(0)
S
20

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

Skippy answered 12/5, 2020 at 2:46 Comment(3)
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.Timbuktu
How silly these CombineLatestX look... Cannot it take an array?Amah
@Amah Yes... #69774995Skippy
U
7

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.

Urge answered 12/5, 2020 at 1:19 Comment(1)
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.0Este
W
4

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()
    }
}
Weihs answered 6/11, 2022 at 12:9 Comment(0)
T
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)
Tonitonia answered 23/9, 2021 at 1:1 Comment(0)
F
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()
    }
Farman answered 24/1, 2022 at 17:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.