Context
I am following the example from WWDC 2019 722 https://developer.apple.com/videos/play/wwdc2019/722/ and WWDC 2019 721 https://developer.apple.com/videos/play/wwdc2019/721/ and making a field with validation the runs an asynchronous network check on a field.
What should happen, as mentioned in the talk, is that the username field should:
Debounce
Show a loading indicator
Perform the network request
End with the network result
Hide the loading indicator
And show or hide a validation message as a result of the network response
I have a prototype that has the debounce, and mocks the network request by using the delay operator. All of this is working well for the most part.
let a = $firstName
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.flatMap { name -> AnyPublisher<String, Never> in
if name == "" {
return Just(name)
.eraseToAnyPublisher()
} else {
return Just(name)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = true})
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = false})
.eraseToAnyPublisher()
}
}
.map { name -> Bool in name != "Q" }
.assign(to: \.isFirstNameValid, on: self)
The debounce waits until the input has paused. The flatMap acts as a conditional branching in the Combine flow of operators: if the value is empty, do not bother with the network request; else, if the value has value after the debounce, perform the network request. Lastly, my example is that "Q" is always an error, for mock purposes.
However, the slight problem is that the debounce happens before the branching. I would like to move the debounce to the else branch of the conditional, like so.
let a = $firstName
.flatMap { name -> AnyPublisher<String, Never> in
if name == "" {
return Just(name)
.eraseToAnyPublisher()
} else {
return Just(name)
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = true})
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = false})
.eraseToAnyPublisher()
}
}
.map { name -> Bool in name != "Q" }
.assign(to: \.isFirstNameValid, on: self)
When this happens, the true branch of the conditional (empty input) is run correctly, and the map+assign after the flatMap run correctly. However, when the input has value, and the else branch of the conditional runs, nothing after the debounce is run at all.
I have tried switching the DispatchQueue to OperationQueue.main and RunLoop.main to no avail.
Keeping the debounce to before the conditional works okay for now, but I'm wondering if I'm doing anything wrong with my attempt to put it in the branch. I'm also wondering if this would be the correct way to do "branching" in operators with Combine, particularly with my use of flatMap and Just().
Any help would be appreciated!
A Just only ever produces one output. Attaching a debounce to it is not going to debounce anything. At best, it will just delay the output of the Just by the debounce interval. At worst, there's a bug preventing it from working at all, which is what it sounds like based on your description.
Related
In this code I am expecting the Empty() publisher to send completion to the .sink subscriber, but no completion is sent.
func testEmpty () {
let x = XCTestExpectation()
let subject = PassthroughSubject<Int, Never>()
emptyOrSubjectPublisher(subject).sink(receiveCompletion: { completion in
dump(completion)
}, receiveValue: { value in
dump(value)
}).store(in: &cancellables)
subject.send(0)
wait(for: [x], timeout: 10.0)
}
func emptyOrSubjectPublisher (_ subject: PassthroughSubject<Int, Never>) -> AnyPublisher<Int, Never> {
subject
.flatMap { (i: Int) -> AnyPublisher<Int, Never> in
if i == 1 {
return subject.eraseToAnyPublisher()
} else {
return Empty().eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
Why does the emptyOrSubjectPublisher not receive the completion?
The Empty completes, but the overall pipeline does not, because the initial Subject has not completed. The inner pipeline in which the Empty is produced (the flatMap) has "swallowed" the completion. This is the expected behavior.
You can see this more easily by simply producing a Just in the flatMap, e.g. Just(100):
subject
.flatMap {_ in Just(100) }
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &cancellables)
subject.send(1)
You know and I know that a Just emits once and completes. But although the value of the Just arrives down the pipeline, there is no completion.
And you can readily see why it works this way. It would be very wrong if we had a potential sequence of values from our publisher but some intermediate publisher, produced in a flatMap, had the power to complete the whole pipeline and end it prematurely.
(And see my https://www.apeth.com/UnderstandingCombine/operators/operatorsTransformersBlockers/operatorsflatmap.html where I make the same point.)
If the goal is to send a completion down the pipeline, it's the subject that needs to complete. For example, you could say
func emptyOrSubjectPublisher (_ subject: PassthroughSubject<Int, Never>) -> AnyPublisher<Int, Never> {
subject
.flatMap { (i: Int) -> AnyPublisher<Int, Never> in
if i == 1 {
return subject.eraseToAnyPublisher()
} else {
subject.send(completion: .finished) // <--
return Empty().eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
[Note, however, that your whole emptyOrSubjectPublisher is peculiar; it is unclear what purpose it is intended to serve. Returning subject when i is 1 is kind of pointless too, because subject has already published the 1 by the time we get here, and isn't going to publish anything more right now. Thus, if you send 1 at the start, you won't receive 1 as a value, because your flatMap has swallowed it and has produced a publisher that isn't going to publish.]
I use a form for a logging page in my app and I have a bind on the footer to display any error, as you can see below :
ContentView.Swift :
Form { Section(footer: Text(self.viewModel.errorMessage))
ViewModel.swift
init() {
self.isCurrentNameValid
.receive(on: RunLoop.main)
.map { $0 ? "" : "username must at least have 5 characters" }
.assign(to: \.errorMessage, on: self)
.store(in: &cancelSet)
}
The problem is the assign in the viewModel is perform in the init so when I launch my app it will display the message even though the user didn't try to write anything yet.
Is there a way to skip first event like in RxSwift where you would just .skip(1) in combine framework?
Insert the .dropFirst() operator.
self.isCurrentNameValid
.dropFirst()
// ... the rest is as before ...
I have one observable (we will call it trigger) that can emit many times in a short period of time. When it emits I am doing a network Request and the I am storing the result with the scan Operator.
My problem is that I would like to wait until the request is finished to do it again. (But as it is now if trigger emits 2 observables it doesn't matter if fetchData has finished or not, it will do it again)
Bonus: I also would like to take only the first each X seconds (Debounce is not the solution because it can be emitting all the time and I want to get 1 each X seconds, it isn't throttle neither because if an observable emits 2 times really fast I will get the first and the second delayed X seconds)
The code:
trigger.flatMap { [unowned self] _ in
self.fetchData()
}.scan([], accumulator: { lastValue, newValue in
return lastValue + newValue
})
and fetchData:
func fetchData() -> Observable<[ReusableCellVMContainer]>
trigger:
let trigger = Observable.of(input.viewIsLoaded, handle(input.isNearBottomEdge)).merge()
I'm sorry, I misunderstood what you were trying to accomplish in my answer below.
The operator that will achieve what you want is flatMapFirst. This will ignore events from the trigger until the fetchData() is complete.
trigger
.flatMapFirst { [unowned self] _ in
self.fetchData()
}
.scan([], accumulator: { lastValue, newValue in
return lastValue + newValue
})
I'm leaving my previous answer below in case it helps (if anything, it has the "bonus" answer.)
The problem you are having is called "back pressure" which is when the observable is producing values faster than the observer can handle.
In this particular case, I recommend that you don't restrict the data fetch requests and instead map each request to a key and then emit the array in order:
trigger
.enumerated()
.flatMap { [unowned self] count, _ in
Observable.combineLatest(Observable.just(count), self.fetchData())
}
.scan(into: [Int: Value](), accumulator: { lastValue, newValue in
lastValue[newValue.0] = newValue.1
})
.map { $0.sorted(by: { $0.key < $1.key }).map { $0.value }}
To make the above work, you need this:
extension ObservableType {
func enumerated() -> Observable<(Int, E)> {
let shared = share()
let counter = shared.scan(0, accumulator: { prev, _ in return prev + 1 })
return Observable.zip(counter, shared)
}
}
This way, your network requests are starting ASAP but you aren't loosing the order that they are made in.
For your "bonus", the buffer operator will do exactly what you want. Something like:
trigger.buffer(timeSpan: seconds, count: Int.max, scheduler: MainScheduler.instance)
.map { $0.first }
I have a network request that can Succeed or Fail
I have encapsulated it in an observable.
I have 2 rules for the request
1) There can never be more then 1 request at the same time
-> there is a share operator i can use for this
2) When the request was Succeeded i don't want to repeat the same
request again and just return the latest value
-> I can use shareReplay(1) operator for this
The problem arises when the request fails, the shareReplay(1) will just replay the latest error and not restart the request again.
The request should start again at the next subscription.
Does anyone have an idea how i can turn this into a Observable chain?
// scenario 1
let obs: Observable<Int> = request().shareReplay(1)
// outputs a value
obs.subscribe()
// does not start a new request but outputs the same value as before
obs.subscribe()
// scenario 2 - in case of an error
let obs: Observable<Int> = request().shareReplay(1)
// outputs a error
obs.subscribe()
// does not start a new request but outputs the same value as before, but in this case i want it to start a new request
obs.subscribe()
This seems to be a exactly doing what i want, but it consists of keeping state outside the observable, anyone know how i can achieve this in a more Rx way?
enum Err: Swift.Error {
case x
}
enum Result<T> {
case value(val: T)
case error(err: Swift.Error)
}
func sample() {
var result: Result<Int>? = nil
var i = 0
let intSequence: Observable<Result<Int>> = Observable<Int>.create { observer in
if let result = result {
if case .value(let val) = result {
return Observable<Int>.just(val).subscribe(observer)
}
}
print("do work")
delay(1) {
if i == 0 {
observer.onError(Err.x)
} else {
observer.onNext(1)
observer.onCompleted()
}
i += 1
}
return Disposables.create {}
}
.map { value -> Result<Int> in Result.value(val: value) }
.catchError { error -> Observable<Result<Int>> in
return .just(.error(err: error))
}
.do(onNext: { result = $0 })
.share()
_ = intSequence
.debug()
.subscribe()
delay(2) {
_ = intSequence
.debug()
.subscribe()
_ = intSequence
.debug()
.subscribe()
}
delay(4) {
_ = intSequence
.debug()
.subscribe()
}
}
sample()
it only generates work when we don't have anything cached, but thing again we need to use side effects to achieve the desired output
As mentioned earlier, RxSwift errors need to be treated as fatal errors. They are errors your stream usually cannot recover from, and usually errors that would not even be user facing.
For that reason - a stream that emits an .error or .completed event, will immediately dispose and you won't receive any more events there.
There are two approaches to tackling this:
Using a Result type like you just did
Using .materialize() (and .dematerialize() if needed). These first operator will turn your Observable<Element> into a Observable<Event<Element>>, meaning instead of an error being emitted and the sequence terminated, you will get an element that tells you it was an error event, but without any termination.
You can read more about error handling in RxSwift in Adam Borek's great blog post about this: http://adamborek.com/how-to-handle-errors-in-rxswift/
If an Observable sequence emits an error, it can never emit another event. However, it is a fairly common practice to wrap an error-prone Observable inside of another Observable using flatMap and catch any errors before they are allowed to propagate through to the outer Observable. For example:
safeObservable
.flatMap {
Requestor
.makeUnsafeObservable()
.catchErrorJustReturn(0)
}
.shareReplay(1)
.subscribe()
I have this code with seems to be correct. But it's only react on first search change. So the code is executed only one time. I tried to add concat(Observable.never()) to my getAl function but it still running only one time. Did I miss something ?
exists = search.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { searchString -> Observable<Bool> in
guard !searchString.isEmpty else {
return Observable.empty()
}
return ServiceProvider.food.getAll(whereFoodName: searchString)
.flatMap({ (result) -> Observable<Bool> in
return Observable.just(result.count > 0)
})
}
Your code just return an Observable. To work with it you should observe it (or rather subscribe to it in Rx terminology)
You'll probably want something like this:
search.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe(onNext: { searchString in
let exists = ServiceProvider.food.getAll(whereFoodName: searchString).count > 0
print("Exists: \(exists)")
// Or do whatever you want with `exists` constant
// You could call a method to update UI
if exists {
self.button.enabled = true
}
})
.disposed(by: disposeBag) //disposeBag should be your property which will be deallocated on deinit