RxSwift repeated action - swift

I'm switching from RAC and want to have a repeated network request, returning different result types depending on the API of the request.
I want to use an interval, but I don't know how to match the return types.
var loop: Observable<Element> {
return Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).map { _ in
// Do network request and return Observable<Element>
}
}
I need to invoke Observerable.interval with type Int - but return Observable. How would I do that?

Use flatMap:
var loop: Observable<Element> {
return Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).flatMap { _ in
return networkRequest() // returns Observable<Element>
}
}

Related

Never return in a Flatmap with Swift's Combine

In RxSwift, a flatMap operator can easily return a non-completing Observable. Let's say we have this (contrived and silly) Observable chain:
let repo = DataRepository()
Observable
.just(Int.random(in: 0 ..< 1000))
.flatMap { num -> Observable<String> in
if num == 42 {
return .never()
}
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
}
With Combine, the closest I can get is something like this (haven't tried to compile, but you get the idea):
Just(Int.random(in: 0 ..< 1000))
.flatMap { num -> AnyPublisher<String, Never> in
if num == 42 {
return Empty<String, Never>(completeImmediately: false).eraseToAnyPublisher()
}
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
.eraseToAnyPublisher()
}
I'm okay-ish with this solution, but I see two problems that I would like to avoid:
1) The Combine solution is somewhat more verbose to achieve the same thing.
2) I have to call eraseToAnyPublisher() on both returned Publishers, else the return types don't match. I believe calling eraseToAnyPublisher() prevents Swift from applying some internal optimizations (I can't find the article I read about this optimization anymore; the information is scarce around this)
Does anyone have a better approach to handling this example scenario?
Try to lift any conditional logic into operators.
Conditions under which you emit something like Observable.never are best captured in a filter, that way you get the "never" behavior for free.
Example:
Just(Int.random(in: 0 ..< 1000))
.filter { $0 != 42 }
.flatMap {
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
}
I don't know enough about the types in DataRepository to know if you need to type erase inside the flatMap closure.
The previous answer does not answer the original question, it only gives suggestions on how to avoid it.
But there are legitimate situations where this behavior is needed, such as a Timer, that you don't want to tick and consume resources.
The code in the question is almost correct, but it needs to be:
Just(Int.random(in: 0 ..< 1000))
.map { num -> AnyPublisher<String, Never> in
if num == 42 {
return Empty<String, Never>(completeImmediately: false).eraseToAnyPublisher()
}
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
.eraseToAnyPublisher()
}
.switchToLatest()
A more practical use case, for example a timer that ticks every second, and can be paused.
let isPaused: AnyPublisher<Bool, Never>
let pausableTimer = isPaused.
.map { isPaused in
if isPaused {
return Empty<Void, Never>()
.eraseToAnyPublisher()
} else {
return Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.map { _ in }
.eraseToAnyPublisher()
}
}
.switchToLatest()
You don't want to use someting like CombineLatest, because the Timer would keep on ticking, while it's paused and the output even is ignored.

How to handle Errors on never ending chain with materialize?

Imagine the following chain where a user wants to save a list of some sort:
var saveChain = userTappedSaveListSubject
.doOnNext { list -> Void in // create pdf version
let pdfFactory = ArticleListPDFFactory()
list.pdf = try pdfFactory.buildPDF(list)
try database.save(list)
}
.flatMap { list in
AuthorizedNetworking.shared.request(.createList(try ListRequestModel(list)))
.filter(statusCode: 201)
.map { _ in list }
}
.doOnNext { list in
list.uploaded = true
try database.save(list)
try Printer().print(list)
}
.materialize()
.share()
On every operator in the chain errors can occur, which would terminate the stream and the user would be unable to retry saving and printing the list (the whole chain gets disposed).
In the end the user should see either a "success" or "failure" screen by binding the observable to a textField:
Observable.of(
saveChain.elements().map { _ in
("List saved!", subtitle: "Saving successfull")
},
saveChain.errors().map { error in
("Error!", subtitle: error.localizedDescription)
})
.merge()
How should the error be handled?
Here's the obvious fix:
let saveChain = userTappedSaveListSubject
.flatMap { list in
Observable.just(list)
.do(onNext: { list -> Void in // create pdf version
let pdfFactory = ArticleListPDFFactory()
list.pdf = try pdfFactory.buildPDF(list)
try database.save(list)
})
.flatMap { list in
AuthorizedNetworking.shared.request(.createList(try ListRequestModel(list)))
.filter(statusCode: 201)
.map { _ in list }
}
.do(onNext: { list in
list.uploaded = true
try database.save(list)
try Printer().print(list)
})
.materialize()
}
.share()
However, there are a host of problems with this code because of the mixed paradigms.
You are passing around a mutable class inside your Observables. This is problematic because it's a functional paradigm so the system expects the contained type to be either a struct/enum or an immutable class.
Your reliance on side effects to load up said mutable class object again is quite odd and against the paradigm.

Swift Combine: How to create a single publisher from a list of publishers?

Using Apple's new Combine framework I want to make multiple requests from each element in a list. Then I want a single result from a reduction of all the the responses. Basically I want to go from list of publishers to a single publisher that holds a list of responses.
I've tried making a list of publishers, but I don't know how to reduce that list into a single publisher. And I've tried making a publisher containing a list but I can't flat map a list of publishers.
Please look at the "createIngredients" function
func createIngredient(ingredient: Ingredient) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
return apollo.performPub(mutation: CreateIngredientMutation(name: ingredient.name, optionalProduct: ingredient.productId, quantity: ingredient.quantity, unit: ingredient.unit))
.eraseToAnyPublisher()
}
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
// first attempt
let results = ingredients
.map(createIngredient)
// results = [AnyPublisher<CreateIngredientMutation.Data, Error>]
// second attempt
return Publishers.Just(ingredients)
.eraseToAnyPublisher()
.flatMap { (list: [Ingredient]) -> Publisher<[CreateIngredientMutation.Data], Error> in
return list.map(createIngredient) // [AnyPublisher<CreateIngredientMutation.Data, Error>]
}
}
I'm not sure how to take an array of publishers and convert that to a publisher containing an array.
Result value of type '[AnyPublisher]' does not conform to closure result type 'Publisher'
Essentially, in your specific situation you're looking at something like this:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
Publishers.MergeMany(ingredients.map(createIngredient(ingredient:)))
.collect()
.eraseToAnyPublisher()
}
This 'collects' all the elements produced by the upstream publishers and – once they have all completed – produces an array with all the results and finally completes itself.
Bear in mind, if one of the upstream publishers fails – or produces more than one result – the number of elements may not match the number of subscribers, so you may need additional operators to mitigate this depending on your situation.
The more generic answer, with a way you can test it using the EntwineTest framework:
import XCTest
import Combine
import EntwineTest
final class MyTests: XCTestCase {
func testCreateArrayFromArrayOfPublishers() {
typealias SimplePublisher = Just<Int>
// we'll create our 'list of publishers' here. Each publisher emits a single
// Int and then completes successfully – using the `Just` publisher.
let publishers: [SimplePublisher] = [
SimplePublisher(1),
SimplePublisher(2),
SimplePublisher(3),
]
// we'll turn our array of publishers into a single merged publisher
let publisherOfPublishers = Publishers.MergeMany(publishers)
// Then we `collect` all the individual publisher elements results into
// a single array
let finalPublisher = publisherOfPublishers.collect()
// Let's test what we expect to happen, will happen.
// We'll create a scheduler to run our test on
let testScheduler = TestScheduler()
// Then we'll start a test. Our test will subscribe to our publisher
// at a virtual time of 200, and cancel the subscription at 900
let testableSubscriber = testScheduler.start { finalPublisher }
// we're expecting that, immediately upon subscription, our results will
// arrive. This is because we're using `just` type publishers which
// dispatch their contents as soon as they're subscribed to
XCTAssertEqual(testableSubscriber.recordedOutput, [
(200, .subscription), // we're expecting to subscribe at 200
(200, .input([1, 2, 3])), // then receive an array of results immediately
(200, .completion(.finished)), // the `collect` operator finishes immediately after completion
])
}
}
I think that Publishers.MergeMany could be of help here. In your example, you might use it like so:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
let publishers = ingredients.map(createIngredient(ingredient:))
return Publishers.MergeMany(publishers).eraseToAnyPublisher()
}
That will give you a publisher that sends you single values of the Output.
However, if you specifically want the Output in an array all at once at the end of all your publishers completing, you can use collect() with MergeMany:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
let publishers = ingredients.map(createIngredient(ingredient:))
return Publishers.MergeMany(publishers).collect().eraseToAnyPublisher()
}
And either of the above examples you could simplify into a single line if you prefer, ie:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
Publishers.MergeMany(ingredients.map(createIngredient(ingredient:))).eraseToAnyPublisher()
}
You could also define your own custom merge() extension method on Sequence and use that to simplify the code slightly:
extension Sequence where Element: Publisher {
func merge() -> Publishers.MergeMany<Element> {
Publishers.MergeMany(self)
}
}
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
ingredients.map(createIngredient).merge().eraseToAnyPublisher()
}
To add on the answer by Tricky, here is a solution which retains the order of elements in the array.
It passes an index for each element through the whole chain, and sorts the collected array by the index.
Complexity should be O(n log n) because of the sorting.
import Combine
extension Publishers {
private struct EnumeratedElement<T> {
let index: Int
let element: T
init(index: Int, element: T) {
self.index = index
self.element = element
}
init(_ enumeratedSequence: EnumeratedSequence<[T]>.Iterator.Element) {
index = enumeratedSequence.offset
element = enumeratedSequence.element
}
}
static func mergeMappedRetainingOrder<InputType, OutputType>(
_ inputArray: [InputType],
mapTransform: (InputType) -> AnyPublisher<OutputType, Error>
) -> AnyPublisher<[OutputType], Error> {
let enumeratedInputArray = inputArray.enumerated().map(EnumeratedElement.init)
let enumeratedMapTransform: (EnumeratedElement<InputType>) -> AnyPublisher<EnumeratedElement<OutputType>, Error> = { enumeratedInput in
mapTransform(enumeratedInput.element)
.map { EnumeratedElement(index: enumeratedInput.index, element: $0)}
.eraseToAnyPublisher()
}
let sortEnumeratedOutputArrayByIndex: ([EnumeratedElement<OutputType>]) -> [EnumeratedElement<OutputType>] = { enumeratedOutputArray in
enumeratedOutputArray.sorted { $0.index < $1.index }
}
let transformToNonEnumeratedArray: ([EnumeratedElement<OutputType>]) -> [OutputType] = {
$0.map { $0.element }
}
return Publishers.MergeMany(enumeratedInputArray.map(enumeratedMapTransform))
.collect()
.map(sortEnumeratedOutputArrayByIndex)
.map(transformToNonEnumeratedArray)
.eraseToAnyPublisher()
}
}
Unit test for the solution:
import XCTest
import Combine
final class PublishersExtensionsTests: XCTestCase {
// MARK: - Private properties
private var cancellables = Set<AnyCancellable>()
// MARK: - Tests
func test_mergeMappedRetainingOrder() {
let expectation = expectation(description: "mergeMappedRetainingOrder publisher")
let numbers = (1...100).map { _ in Int.random(in: 1...3) }
let mapTransform: (Int) -> AnyPublisher<Int, Error> = {
let delayTimeInterval = RunLoop.SchedulerTimeType.Stride(Double($0))
return Just($0)
.delay(for: delayTimeInterval, scheduler: RunLoop.main)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
let resultNumbersPublisher = Publishers.mergeMappedRetainingOrder(numbers, mapTransform: mapTransform)
resultNumbersPublisher.sink(receiveCompletion: { _ in }, receiveValue: { resultNumbers in
XCTAssertTrue(numbers == resultNumbers)
expectation.fulfill()
}).store(in: &cancellables)
waitForExpectations(timeout: 5)
}
}
You can do it in one line:
.flatMap(Publishers.Sequence.init(sequence:))

RxSwift, Share + retry mechanism

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()

How is the shareReplay function working?

I have read RxSwift/ShareReplayScope.swift file, but a little difficult to understand.
public func share(replay: Int = 0, scope: SubjectLifetimeScope = .whileConnected)
-> Observable<E> {
switch scope {
case .forever:
switch replay {
case 0: return self.multicast(PublishSubject()).refCount()
default: return self.multicast(ReplaySubject.create(bufferSize: replay)).refCount()
}
case .whileConnected:
switch replay {
case 0: return ShareWhileConnected(source: self.asObservable())
case 1: return ShareReplay1WhileConnected(source: self.asObservable())
default: return self.multicast(makeSubject: { ReplaySubject.create(bufferSize: replay) }).refCount()
}
}
}
0,1 and default,what is the difference? why separate 1 from defalut?
override func subscribe<O : ObserverType>(_ observer: O) -> Disposable where O.E == E {
_lock.lock()
let connection = _synchronized_subscribe(observer)
let count = connection._observers.count
let disposable = connection._synchronized_subscribe(observer)
_lock.unlock()
if count == 0 {
connection.connect()
}
return disposable
}
how the lock work, and the most difficult is this function. how the blocked obserables connect to their observers correctly.
Below you get the information about shareReplay function.
share()
As you know share function shares the subscription of Observable. So that you don't need to create multiple observable instances every time.
Simply create one observable and share replay. it will allow to perform next operation within this sharable observable. i.e filter(), subscribe(), etc..
But the problem with share() is, it is not provide values before subscription.
shareReplay()
Where shareReplay() keeps a buffer of the last few emitted values and can provide them to new observers upon subscription.