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

I'm trying 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 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.
How can I create a reusable publisher so that I don't have to repeat these inline publishers which all do the same thing?

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

Swift Combine: Merge publishers of different types

I am trying to merge multiple publishers that are of different types.
I have publishers of type string and a publisher of type, however when I merge them using MergeMany or CombineLatest I get a type mismatch error.
Is there anyway to merge publishers of different types? See the code example below:
#Published var str1: String?
#Published var str2: String?
#Published var image: Image?
Publishers.MergeMany($str1, $str2, $image)
.removeDuplicates()
.sink { _ in
//...
}
.store(in: &bag)
Assuming that you had no other #Published properties, the easiest thing to do would be to use objectWillChange:
self.objectWillChange
.receive(on: RunLoop.main)
.sink {
print("objectWillChange...", self.str1, self.str2, self.image)
}
.store(in: &bag)
However, this does not give you the remove duplicates that you mentioned in the comments.
If you want, that, you can get a little more complicated with things:
Publishers.MergeMany(
$str1.dropFirst().removeDuplicates().map({ _ in }).eraseToAnyPublisher(),
$str2.dropFirst().removeDuplicates().map({ _ in }).eraseToAnyPublisher(),
$image.dropFirst().removeDuplicates().map({ _ in }).eraseToAnyPublisher()
)
.receive(on: RunLoop.main)
.sink {
print("MergeMany...", self.str1, self.str2, self.image)
}
.store(in: &bag)
(There may be a prettier way than my map. { _ in } to remove the return type)
The above method seems to work effectively for removing the individual duplicate elements and only triggering the sink when one of them has changed.
#jnpdx's answer gets the job done. I'm focusing on this question from OP:
Is there anyway to merge publishers of different types?
It is important to realize the difference between Merge and CombineLatest operators since they are quite different, semantically.
Check out RxMarbles for Merge and CombineLatest.
Merge requires the inner publishers to have the same Output and emits a single value whenever one emits.
CombineLatest publishes a tuple containing the latest values for each inner publisher.
I would write this like so:
#Published var str1: String?
#Published var str2: String?
#Published var image: Image?
func setUpBindings() {
Publishers.CombineLatest3($str1, $str2, $image)
.sink { _ in
//...
}
.store(in: &bag)
}
Since tuples can't conform to Equatable (yet), you can't leverage removeDuplicates() on the resulting publisher.
Like #jnpdx wrote, an approach could be to pull the .removeDuplicates() inside to each publisher.
Publishers.CombineLatest3(
$str1.removeDuplicates(),
$str2.removeDuplicates(),
$image.removeDuplicates()
)
.sink { (str1, str2, image) in

Use result of publisher in map of another publisher

As example I have a basic published value like
#Published var value: String
I have want to validates this value of my form to give my user an output. For that I will use Combine in my MVVM project.
Now this type of value needs to be validated against my REST API. For my REST API I already have a method to get my results of my like getFirstMailboxRedirectFromAPI which returns AnyPublisher<MailboxAPIResponse, APIError>. MailboxAPIResponse is a decodable object for the api response. So if I just want to display the result, I create a subscriber with .sink add the result to a variable which will be shown in a view. So good so far. Now my problem:
As described in the first section my value is already a Publisher (because of #Published), where I can do some .map stuff for validation with it and returning true or false if everything is fine or not.
So to validate my published value I need to call my other Publisher which uses the API to check if the value already exists. But I don't know how this should work.
This is my code so far but this doesn't work. But this shows you my idea how it should work.
private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
$mailboxRedirectName
.debounce(for: 0.5, scheduler: RunLoop.main)
.setFailureType(to: Error.self)
.flatMap { name in
self.getFirstMailboxRedirectFromAPI(from: name, and: self.domainName)
.map { apiResponse in
return apiResponse.response.data.count == 0
}
}
.eraseToAnyPublisher()
}
So in result the publisher should use the redirectName to call the API and the API gives me the result if the mailbox already exists, then returns a boolean, if it's existing or not.
How can I nest multiple publishers and use the result of the API publisher in my published value publisher?
I simplified a little but key take aways are 1) use switchToLatest to flatten a Publisher of Publishers if you want the operation to restart (flatMap is a merge, so events could arrive out of order). 2) You need to handle the failure types and make sure an inner Publisher never fails or the outer publisher will also complete.
final class M: ObservableObject {
#Published var mailboxRedirectName: String = ""
private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
$mailboxRedirectName
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { [weak self] name -> AnyPublisher<Bool, Never> in
guard let self = self else { return Just(false).eraseToAnyPublisher() }
return self
.getFirstMailboxRedirectFromAPI(from: name)
.replaceError(with: false)
.eraseToAnyPublisher()
}
.switchToLatest()
.eraseToAnyPublisher()
}
func getFirstMailboxRedirectFromAPI(from name: String) -> AnyPublisher<Bool, Error> {
Just(true).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}

RxSwift `ActivityIndicator` Functionality in Combine

I've been working with RxSwift for a few years now, and am starting to explore Combine with SwiftUI and am having some trouble trying to replicate some functionality from RxSwift in Combine.
On the RxSwift GitHub there is an example in a file called ActivityIndicator.swift.
Basic usage is as follows:
class Foo {
let activityIndicator = ActivityIndicator()
lazy var activity = activityIndicator.asDriver()
var disposeBag = DisposeBag()
func doSomething() {
Observable
.just("this is something")
.trackActivity(activityIndicator)
.subscribe()
.disposed(by: disposeBag)
}
}
What this does is allow you to then drive off of the activity driver and it will emit boolean values every time something subscribes or a subscription completes.
You can then directly drive something like a UIActivityIndicatorView's isAnimating property using RxCocoa.
I've been trying to figure out how to create something similar to this in Combine but am not having any luck.
Say I have a viewModel that looks like this:
class ViewModel: ObservableObject {
#Published var isActive = false
func doSomething() -> AnyPublisher<Void, Never> {
Just(())
.delay(for: 2.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
What I would like to do is create an operator for a Publisher that will function similarly to how the Rx operator worked where I can forward the events from the subscription through the chain, but change the isActive value every time something subscribes/completes/cancels.
In the SwiftUI View I would initiate the doSomething function and sink to it, while also being able to use the published isActive property to show/hide a ProgressView
Something similar to this:
struct SomeView: View {
let viewModel = ViewModel()
var body: some View {
var cancelBag = Set<AnyCancellable>()
VStack {
Text("This is text")
if viewModel.isActive {
ProgressView()
}
}
.onAppear(perform: {
viewModel
.doSomething()
.sink()
.store(in: &cancelBag)
})
}
}
Is there something that works like this already that I am just completely missing?
If not, how can I go about replicating the RxSwift functionality in Combine?
Thank you in advance for the help.
Looks like someone created a Combine version. I don't know if it has the same issue as discussed by #Daniel T. but it looks promising.
https://github.com/duyquang91/ActivityIndicator
Hmm... The key to the ActivityIndicator class is the Observable.using(_:observableFactory:) operator. Unfortunately, I don't believe there is an equivalent operator in Combine.
The using operator creates a resource when the Observable is subscribed to, and then disposes the resource when the Observable sends a stop event (complete or error.) This insures the resource's lifetime. In this particular case, the resource just increments an Int value on creation and decrements it on disposal.
I think you could kind of mimic the behavior with something like this:
extension Publisher {
func trackActivity(_ activityIndicator: CombineActivityIndicator) -> some Publisher {
return activityIndicator.trackActivity(of: self)
}
}
final class CombineActivityIndicator {
var counter = CurrentValueSubject<Int, Never>(0)
var cancelables = Set<AnyCancellable>()
func trackActivity<Source: Publisher>(of source: Source) -> some Publisher {
let sharedSource = source.share()
counter.value += 1
sharedSource
.sink(
receiveCompletion: { [unowned self] _ in
self.counter.value -= 1
},
receiveValue: { _ in }
)
.store(in: &cancelables)
return sharedSource
}
var asPublisher: AnyPublisher<Bool, Never> {
counter
.map { $0 > 0 }
.eraseToAnyPublisher()
}
}
However, the above class will heat up the Publisher and you might miss emitted values because of it. Use at your own risk, I do not recommend the above unless you are desperate.
Maybe someone has written a using operator for Publisher and will be willing to share.

Extension for default empty value for optional publisher?

I'm trying to pass in an optional publisher to my view's .onReceive. I don't want to force wrap it like this:
let refreshPublisher: AnyPublisher<Void, Never>?
var body: some View {
Group {
...
}
.onReceive(refreshPublisher!) {...}
}
So I'm passing an empty default value like this:
var body: some View {
Group {
...
}
.onReceive(refreshPublisher ?? Empty<Void, Never>().eraseToAnyPublisher()) {...}
}
This is really verbose and want to add an extension on Optional publishers something like this:
extension Optional where Wrapped == Publisher { // Incorrect syntax, doesn't compile
func orEmpty() -> AnyPublisher<Void, Never> { ... }
}
This way, I can end or doing something like this:
var body: some View {
Group {
...
}
.onReceive(refreshPublisher.orEmpty()) {...}
}
Is something like this possible or a better way to handle optional publishers in non-optional chains?
To your specific question, it's certainly possible. I just wouldn't do it this way.
Note the addition of Combine. before Publisher. There's a type called Optional.Publisher that shadows Combine.Publisher and that's what is causing your error.
extension Optional where Wrapped: Combine.Publisher {
func orEmpty() -> AnyPublisher<Wrapped.Output, Wrapped.Failure> {
self?.eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher()
}
}
Instead of doing it this way, I'd make the default value Empty and not use Optional at all:
let refreshPublisher: AnyPublisher<Void, Never>
init(refreshPublisher: AnyPublisher<Void, Never> = Empty().eraseToAnyPublisher()) {
self.refreshPublisher = refreshPublisher
}
I'd also rework this so that the caller can pass any publisher they want rather than forcing AnyPublisher on them:
let refreshPublisher: AnyPublisher<Void, Never>
init<Refresh>(refreshPublisher: Refresh) where Refresh: Publisher,
Refresh.Output == Void, Refresh.Failure == Never {
self.refreshPublisher = refreshPublisher.eraseToAnyPublisher()
}
init() {
self.init(refreshPublisher: Empty())
}

How to convert a publisher to a CurrentValueSubject?

I have a publisher that my view's onReceive is subscribed to. Instead of duplicating logic in onAppear as well, I'd like the publisher in the onReceive to fire on first subscribe.
Is there a way to convert a publisher to a CurrentValueSubject? Here's what I'm trying to do:
var myPublisher: CurrentValueSubject<Void, Never> {
Just(()) // Error: Cannot convert return expression of type 'Just<()>' to return type 'CurrentValueSubject<Void, Never>'
}
Is this possible or a better way to do this?
You can't convert publishers to CurrentValueSubject at will because it's a very specific type of publisher. Ask yourself whether you really need the type of myPublisher to be CurrentValueSubject<Void, Never> or if an AnyPublisher<Void, Never> would do.
var myPublisher: AnyPublisher <Void, Never> {
Just(()).eraseToAnyPublisher()
}
Alternatively, if all you're trying to do is create an instance of CurrentValueSubject that has () as its initial value you could use this:
var myPublisher: CurrentValueSubject<Void, Never> {
CurrentValueSubject<Void, Never>(())
}
You can assign a publisher to a CurrentValueSubject which you can then use in your code. Without duplicating the logic.
var oldPublisher: AnyPublisher <Void, Never>
private var cancellables = Set<AnyCancellable>()
var myCurrentValueSubject: CurrentValueSubject<Void, Never> {
let subject = CurrentValueSubject<Void, Never>(())
oldPublisher
.assign(to: \.value, on: subject)
.store(in: &cancellables)
return subject
}