SwiftUI and Combine, how to create a reusable publisher to check if a string is empty

Viewed 2205

I'm struggling to learn SwiftUI and Combine syntax and am trying to understand how to create a reusable publisher that will check if a String is empty.

I've got a SwiftUI with 5 TextFields which using @Binding to connect them to my data model object.

class DataWhatIsLoanPayment: ObservableObject {
    // Input
    @Published var pv = ""
    @Published var iyr = ""
    // a bunch more fields...

    // Output
    @Published var isvalidform = false
}

I want to enable the Calculate button once all of the fields are filled in (isEmpty == false).

I'm following along with https://peterfriese.dev/swift-combine-love/, and I was able to get my SwiftUI to properly enable/disable my Calculate button by creating an isValidPVPublisher and an isValidIYRPublisher and combing them in an isValidFormPublisher, like so:

private var isValidPVPublisher: AnyPublisher<Bool, Never> {
    $pv
        .debounce(for: 0.8, scheduler: RunLoop.main)
        .removeDuplicates()
        .map { input in
            return input.isEmpty == false
        }
        .eraseToAnyPublisher()
}

private var isValidIYRPublisher: AnyPublisher<Bool, Never> {
    $iyr
        .debounce(for: 0.8, scheduler: RunLoop.main)
        .removeDuplicates()
        .map { input in
            return input.isEmpty == false
        }
        .eraseToAnyPublisher()
}


private var isValidFormPublisher: AnyPublisher<Bool, Never> {
    Publishers.CombineLatest(isValidPVPublisher, isValidIYRPublisher)
        .map { pvIsValid, iyrIsValid in
            return pvIsValid && iyrIsValid
        }
        .eraseToAnyPublisher()
}

init() {        
    isValidFormPublisher
        .receive(on: RunLoop.main)
        .assign(to: \.isValidForm, on: self)
        .store(in: &cancellableSet)
}

However, I'm going to have a lot more than 2 fields, and I'm going to have a lot of other forms in my app in which I will want to check if my fields are empty. And repeating .debounce(for: 0.8, scheduler: RunLoop.main).removeDuplicates().map { input in return input.isEmpty == false }.eraseToAnyPublisher() over and over again is a bad idea.

I want to create a reusable NotEmptyPublisher, or something like that, which takes a field binding, like my $pv and sets up the chain as show in the isValidPVPublisher above. So I can have something like:

// Something like this, but I'm not sure of the syntax...
private var isValidPVPublisher = NotEmptyPublisher(field:$pv)
// instead of ...
private var isValidPVPublisher: AnyPublisher<Bool, Never> {
    $pv
        .debounce(for: 0.8, scheduler: RunLoop.main)
        .removeDuplicates()
        .map { input in
            return input.isEmpty == false
        }
        .eraseToAnyPublisher()
}

But I'm having a great deal of trouble parsing a lot of Swift syntax that I'm not familiar with and I can't seem to figure out how to do it, and every example I find on the web is just defining the publisher chain inline instead of in a reusable fashion.

Any help? How can I create a reusable publisher so that I don't have to repeat these inline publishers which all do the same thing?

1 Answers

Here you are!

extension Publisher where Output == String {
    func isStringInhabited() -> Publishers.Map<Self, Bool> {
        map { !$0.isEmpty }
    }
}

$0 is shorthand for the first argument to the closure, $1 means the second, and so on and so forth.

! is the Bool inversion operator, prefixing ! is shorthand for suffixing == false.

Now, as to your question about reuse, you don't need to overkill things that hard, you can just create a function.

private func isValidTransform<P: Publisher>(input: P) -> some Publisher where P.Output == String {
    input
        .debounce(for: 0.8, scheduler: RunLoop.main)
        .removeDuplicates()
        .isStringInhabited()
}

P is a generic, which means it could be any type whatsoever as long as that type conforms to Publisher. The where clause allows us to constrain this conformance further, denoting that we can only operate on Publishers when their Output is String. some Publisher gives us an opaque return type to save us from having to write the type signature of a Publisher that has been transformed multiple times, you can change this to AnyPublisher<Bool, Never> and use .eraseToAnyPublisher() if you like but I recommend only using that erasure at the time of need.

Related