How to handle Nested and Paginated Network Call in Swift Combine - swift

I'm using the Notion api which returns all the data I need in Block objects. The struggle is that each Block can be paginated and/or have children that is an array of Block.
Basic Block Structure:
struct Block {
var id: String
var data: [Data]
var hasMore: Bool
var hasChildren: Bool
var childrenId: String
var nextPageId: String
var children: [Block]? = nil
// used for pagination to append data
mutating func accumulator(newRes: BlockResponse) -> BlockResponse {
let combinedArr = self.results + newRes.results
var combinedRes = newRes
combinedRes.results = combinedArr
self = combinedRes
return self
}
}
What I am trying to do:
Pass in list of Block Ids
Get all of the Block data for each ID including all paginated and nested data
Note:
The paginated data should be stored in the original Block data field
The children data should be stored in their parent Block children field
Each child Block can also have paginated data and/or children data
I've gotten semi-close, but I just can't figure out how to get the nested and paginated children data (the last function below). I'm sure I am way overcomplicating this, but right now it basically returns everything properly except the nested children data (it only returns 1 level deep).
Current Code
func getAllBlockData() -> AnyPublisher<[Block], Never> {
return loadAllBlockIds() // just returns a list of ids
.map { $0.results.map { $0 } } // getting the ids from a list of ids
.flatMap(\.publisher)
.flatMap(loadBlockData)
.collect()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
func loadBlockData(forId id: String) -> AnyPublisher<Block, Never> {
return loadBlock(id: id)
.flatMap(getMoreBlockData)
.flatMap(getChildData)
.eraseToAnyPublisher()
}
func loadBlock(forId id: String) -> AnyPublisher<Block, Never> {
// basic code to get the block data
}
func getMoreBlockData(forBlock block: Block) -> AnyPublisher<Block, Never> {
if block.hasMore {
return loadMoreBlockData(forBlock: block)
}
return Just(block).eraseToAnyPublisher()
}
func getChildData(forBlock block: Block) -> AnyPublisher<Block, Never> {
if block.hasChildren {
return loadChildData(forBlock: block)
}
return Just(block).eraseToAnyPublisher()
}
// Recursively calls its inner function to accumulate the paginated data
func loadMoreBlockData(forBlock block: Block) -> AnyPublisher<Block, Never> {
func doRequest(forId id: String, accumulator: Block) -> AnyPublisher<Block, Never> {
return loadBlock(id: id)
.flatMap { blockRes -> AnyPublisher<Block, Never> in
var mutableAccumulator = accumulator
if blockRes.hasMore {
return doRequest(forId: blockRes.moreId!, accumulator: mutableAccumulator.accumulator(newRes: blockRes))
}
return Just(mutableAccumulator.accumulator(newRes: blockRes)).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
return doRequest(forId: block.moreId!, accumulator: block)
}
// This is causing me the problems (well, at least the major ones)
func loadChildData(forBlock block: Block) -> AnyPublisher<Block, Never> {
var mutableBlock = block
func doRequest(forId id: String) -> AnyPublisher<Block, Never> {
return loadBlock(id: id)
.flatMap(getMoreBlockData)
.flatMap({ childRes -> AnyPublisher<Block, Never> in
if mutableBlock.children != nil {
mutableBlock.children?.append(childRes)
} else {
mutableBlock.children = [childRes]
}
return Just(childRes).eraseToAnyPublisher()
})
.flatMap(getChildData)
.flatMap { _ -> AnyPublisher<BlockResponse, Never> in
return Just(mutableBlock).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
return doRequest(forId: block.childId!)
}

Related

How to filter an Actor object?

I have an Actor object that I want to be able to iterate and filter.
actor DataModel {
typealias Details = (passed: Bool, scores: [Int])
private(set) var data: [Int: Details] = [:]
func update(_ value: (Bool, [Int]), forKey key: Int) {
data.updateValue(value, forKey: key)
}
subscript(id: Int) -> Details? {
get {
data[id]
}
set {
data[id] = newValue
}
}
func removeAll() {
data.removeAll()
}
}
extension DataModel: AsyncSequence, AsyncIteratorProtocol {
typealias Element = (key: Int, value: Details)
func next() async throws -> Element? {
var iterator = data.makeIterator()
return iterator.next()
}
nonisolated func makeAsyncIterator() -> Data {
self
}
}
let data = DataModel()
await data.update((false, [1, 2]), forKey: 0)
But, whenever I use the filter method, it goes into an infinite loop.
let filtered = data.filter { el in
/// infinite loop
return el.value.passed || el.value.scores.count > 3
}
for try await i in filtered {
print(i)
}
Update
Created a separate iterator, but getting the following error:
Actor-isolated property 'data' can not be referenced from a non-isolated context
extension DataDetail: AsyncSequence {
typealias Element = (key: Int, value: (passed: Bool, scores: [Int]))
typealias AsyncIterator = DataInterator
nonisolated func makeAsyncIterator() -> DataInterator {
return DataInterator(data) /// Actor-isolated property 'data' can not be referenced from a non-isolated context
}
}
struct DataInterator: AsyncIteratorProtocol {
typealias Detail = (key: Int, value: (passed: Bool, scores: [Int]))
private let details: [Int: (passed: Bool, scores: [Int])]
lazy var iterator = details.makeIterator()
init(_ details: [Int: (passed: Bool, scores: [Int])]) {
self.details = details
}
mutating func next() async throws -> Detail? {
let nextDetail = iterator.next()
return nextDetail
}
}
You have a mistake in your next() method. You're creating a new iterator on each call, so every call to your next() method is effectively returning data.first over and over again. It'll never hit nil, so it'll never end.
I'm not sure what the easiest way to fix it is, however. You can't just return data.makeIterator() from makeAsyncIterator(), because data is actor-isolated.
You'll probably want to make a new AsyncIteratorProtocol-conforming struct which wraps your actor and vends the elements of its data in an actor-isolated way

Combine - bind a stream into another and handle side effects while doing it

I am trying to learn Combine. I know the terms and the basic concept theoretically. But when trying to work with it, I am lost.
I am trying to do is map an Input stream of events to Output stream of state. Is there a way to bind the result of the map to outputSubject? I am trying to make it work with sink but is there a better way?
Also is there an operator equivalent of RxSwift's withLatestFrom?
import Combine
class LearnCombine {
typealias Input = PassthroughSubject<Event, Never>
typealias Ouput = AnyPublisher<State, Never>
let input: Input
var output: Ouput
private var outputSubject: CurrentValueSubject<State, Never>
private var cancellables = Set<AnyCancellable>()
init() {
self.input = PassthroughSubject()
self.outputSubject = CurrentValueSubject(.initial)
self.output = outputSubject.eraseToAnyPublisher()
transformPipeline()
}
private func transformPipeline() {
input
.map { event in
mapEventToState(event, with: outputSubject.value)
}
.handleOutput { state in
handleSideEffects(for: state) // Also, how do I access the event here if I needed?
}
.sink {
outputSubject.send($0)
}
.store(in: &cancellables)
}
func mapEventToState(_ event: Event, with state: State) -> State {
// Some code that converts `Event` to `State`
}
}
extension Publisher {
func handleOutput(_ receiveOutput: #escaping ((Self.Output) -> Void)) -> Publishers.HandleEvents<Self> {
handleEvents(receiveOutput: receiveOutput)
}
}
Instead of using sink to assign a value to a CurrentValueSubject, I would use assign.
If you want to do something with the values in the middle of a pipeline you can use the handleEvents operator, though if you look in the documentation you'll see that the operator is listed as a debugging operator because generally your pipeline should not have side effects (building it from pure functions is one of the primary benefits.
Just reading the description of withLatestFrom in the RX documentation, I think the equivalent in combine is combineLatest
Here's your code, put into a Playground, and modified a bit to illustrates the first two points:
import Combine
struct Event {
var placeholder: String
}
enum State {
case initial
}
class LearnCombine {
typealias Input = PassthroughSubject<Event, Never>
typealias Ouput = AnyPublisher<State, Never>
let input: Input
var output: Ouput
private var outputSubject: CurrentValueSubject<State, Never>
private var cancellables = Set<AnyCancellable>()
init() {
self.input = PassthroughSubject()
self.outputSubject = CurrentValueSubject(.initial)
self.output = outputSubject.eraseToAnyPublisher()
transformPipeline()
}
private func transformPipeline() {
input
.map { event in
self.mapEventToState(event, with: self.outputSubject.value)
}
.handleEvents(receiveOutput: { value in
debugPrint("Do something with \(value)")
})
.assign(to: \.outputSubject.value, on: self)
.store(in: &cancellables)
}
func mapEventToState(_ event: Event, with state: State) -> State {
return .initial
// Some code that converts `Event` to `State`
}
}
extension Publisher {
func handleOutput(_ receiveOutput: #escaping ((Self.Output) -> Void)) -> Publishers.HandleEvents<Self> {
handleEvents(receiveOutput: receiveOutput)
}
}

Saving string in flatMap block to database in api call using Combine Swift

I am trying to fetch a value from local database and when not found wants to save it to local database and return it. All of these I am doing in Interactor file and actual saving or fetching is done in seperate file. Following is my code:
public func fetchCode(codeId: String) -> AnyPublisher<String?, Error> {
//Get code from localdb
codeStorageProvider.fetchCode(codeId).flatMap { (code) -> AnyPublisher<String?, Error> in
if let code = code {
return Just(code).mapError{ $0 as Error }.eraseToAnyPublisher()
}
//If not found in db, Get code from server
let code = self.voucherCodeProvider.fetchVoucherCode(codeId: codeId)
return code.flatMap { code in
//save the code to local db
self.codeStorageProvider.saveVoucherCode(code, codeId)
return code
}.eraseToAnyPublisher()
//return code to presenter
}.eraseToAnyPublisher()
}
I am getting following error in flatMap:
Type of expression is ambiguous without more context
Can someone please help me?
If your saveVoucher doesn't return a Publisher and you are not interested in knowing when the operation is completed, there's no need to use flatMap but you can use handleEvents and call the side effect to save the code from there. Something like this:
func fetchLocal(codeId: String) -> AnyPublisher<String?, Error> {
return Empty().eraseToAnyPublisher()
}
func fetchRemote(codeId: String) -> AnyPublisher<String, Error> {
return Empty().eraseToAnyPublisher()
}
func saveLocal(code: String, codeId: String) {
// Save to BD
}
func fetch(codeId: String) -> AnyPublisher<String?, Error> {
return fetchLocal(codeId: codeId)
.flatMap { code -> AnyPublisher<String, Error> in
if let code = code {
return Just(code)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return fetchRemote(codeId: codeId)
.handleEvents(receiveOutput: {
saveLocal(code: $0, codeId: codeId)
})
.eraseToAnyPublisher()
}
}
.map(Optional.some)
.eraseToAnyPublisher()
}

Swift Combine return result of first future after evaluation of second

I have the following situation:
2 futures, one returns a value I am interested in, the other does some operations and returns void. The 2 are not related to each other (so the code should not be mixed), but both need to be executed in the right order in the application logic.
What I want to do is subscribe to a publisher that does the following:
future one executes and gives a value
future two executes and returns nothing
the subscriber receives the value of future one after the execution of future two.
Here is a small code example that does not compile, that shows what I would like to achieve:
import Combine
func voidFuture() -> Future<Void, Error> {
return Future<Void, Error> { promise in
promise(.success(()))
}
}
func intFuture() -> Future<Int, Error> {
return Future<Int, Error> { promise in
promise(.success(1))
}
}
func combinedFuture() -> AnyPublisher<Int, Error> {
var intValue: Int!
return intFuture().flatMap { result in
intValue = result
return voidFuture()
}.flatMap{ _ in
return CurrentValueSubject(intValue).eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
var subscriptions = Set<AnyCancellable>()
combinedFuture()
.sink(receiveCompletion: { _ in }, receiveValue: { val in print(val)})
.store(in: &subscriptions)
You need to .map the Void result of the second publisher (voidFuture) back to the result of the first publisher (intFuture), which is what the .flatMap would emit:
func combinedFuture() -> AnyPublisher<Int, Error> {
intFuture().flatMap { result in
voidFuture().map { _ in result }
}
.eraseToAnyPublisher()
}

Asynchronous iteration using Swift Combine

I am trying to do multiple async operations, in sequence, on an array of data. However I am having problems with the return values of map.
Here is the test code:
import Combine
func getLength(_ string: String) -> Future<Int,Error> {
return Future<Int,Error>{ promise in
print("Length \(string.count)")
promise(.success(string.count))
}
}
func isEven(_ int: Int) -> Future<Bool,Error> {
return Future<Bool,Error>{ promise in
print("Even \(int % 2 == 0)")
promise(.success(int % 2 == 0))
}
}
let stringList = ["a","bbb","c","dddd"]
func testStrings(_ strings:ArraySlice<String>) -> Future<Void,Error> {
var remaining = strings
if let first = remaining.popFirst() {
return getLength(first).map{ length in
return isEven(length)
}.map{ even in
return testStrings(remaining)
}
} else {
return Future { promise in
promise(.success(()))
}
}
}
var storage = Set<AnyCancellable>()
testStrings(ArraySlice<String>(stringList)).sink { _ in } receiveValue: { _ in print("Done") }.store(in: &storage)
This generates the following error:
error: MyPlayground.playground:26:11: error: cannot convert return expression of type 'Publishers.Map<Future<Int, Error>, Future<Void, Error>>' to return type 'Future<Void, Error>'
}.map{ even in
I thought we could use map to convert from one publisher type to the other, but it seems it's wrapped inside a Publishers.Map. How do I get rid of this?
Thanks!
Well it seems that this works:
import Combine
func getLength(_ string: String) -> Future<Int,Error> {
return Future<Int,Error>{ promise in
print("Length \(string.count)")
promise(.success(string.count))
}
}
func isEven(_ int: Int) -> Future<Bool,Error> {
return Future<Bool,Error>{ promise in
print("Even \(int % 2 == 0)")
promise(.success(int % 2 == 0))
}
}
let stringList = ["a","bbb","c","dddd"]
func testStrings(_ strings:ArraySlice<String>) -> AnyPublisher<Void,Error> {
var remaining = strings
if let first = remaining.popFirst() {
return getLength(first).flatMap{ length in
return isEven(length)
}.flatMap{ even in
return testStrings(remaining)
}.eraseToAnyPublisher()
} else {
return Future<Void,Error> { promise in
promise(.success(()))
}.eraseToAnyPublisher()
}
}
var storage = Set<AnyCancellable>()
testStrings(ArraySlice<String>(stringList)).sink { _ in } receiveValue: { _ in print("Done") }.store(in: &storage)