Swift - Combine subscription not being called - swift

Recently, I tried to use freshOS/Networking swift package.
And I read the README file several times and I couldn't make it work with me. I'm trying to get a list of countries using public API services and here's what I did:
Model
import Foundation
import Networking
struct CountryModel: Codable {
let error: Bool
let msg: String
let data: [Country]
}
struct Country: Codable {
let name: String
let Iso3: String
}
extension Country: NetworkingJSONDecodable {}
extension CountryModel: NetworkingJSONDecodable {}
/*
Output
{
"error":false,
"msg":"countries and ISO codes retrieved",
"data":[
{
"name":"Afghanistan",
"Iso2":"AF",
"Iso3":"AFG"
}
]
}
*/
View Controller + print(data) in callAPI() function does not print
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
callAPI()
}
fileprivate func configureUI() {
title = "Choose Country"
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.frame = view.bounds
}
fileprivate func callAPI() {
let countriesService = CountriesApi()
var cancellable = Set<AnyCancellable>()
countriesService.countries().sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished") // not printing
break
case .failure(let error):
print(error.localizedDescription)
}
}) { data in
print(data) // not printing
self.countriesData = data
}.store(in: &cancellable)
}
CountriesAPI()
struct CountriesApi: NetworkingService {
let network = NetworkingClient(baseURL: "https://countriesnow.space/api/v0.1")
// Create
func create(country c: Country) -> AnyPublisher<Country, Error> {
post("/countries/create", params: ["name" : c.name, "Iso3" : c.Iso3])
}
// Read
func fetch(country c: Country) -> AnyPublisher<Country, Error> {
get("/countries/\(c.Iso3)")
}
// Update
func update(country c: Country) -> AnyPublisher<Country, Error> {
put("/countries/\(c.Iso3)", params: ["name" : c.name, "Iso3" : c.Iso3])
}
// Delete
func delete(country c: Country) -> AnyPublisher<Void, Error> {
delete("/countries/\(c.Iso3)")
}
func countries() -> AnyPublisher<[CountryModel], Error> {
get("/countries/iso")
}
}
I hope someone can help with what I'm missing.

The problem lies in your callAPI() function, if you change your code to this:
fileprivate func callAPI() {
let countriesService = CountriesApi()
var cancellable = Set<AnyCancellable>()
countriesService.countries()
.print("debugging")
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished") // not printing
break
case .failure(let error):
print(error.localizedDescription)
}
}) { data in
print(data) // not printing
self.countriesData = data
}.store(in: &cancellable)
}
Notice I just added the line print("debugging").
If you run that, you can see in your console that your subscription gets cancelled immediately.
debugging: receive cancel
Why? Because your "cancellable" or "subscription" only lives in the scope of your function, thus, it is deallocated immediately.
What you can do is add the cancellables set as a property in your ViewController, like this:
final class ViewController: UIViewController {
private var cancellable = Set<AnyCancellable>()
fileprivate func callAPI() {
// the code you had without the set
let countriesService = CountriesApi()
countriesService.countries().sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished") // not printing
break
case .failure(let error):
print(error.localizedDescription)
}
}) { data in
print(data) // not printing
self.countriesData = data
}.store(in: &cancellable)
}
}

Related

How fetch data to a #Published array with a completion from Core Data within a performBackgroundTask block?

I'd like to fetch data to a #Published array with a completion from Core Data within a performBackgroundTask block. If have to work with a lot of data, it could blocks the GUI so I want to perform database operations in the background. The GUI is written in SwiftUI.
//
// ContentView.swift
// Shared
//
// Created by DEV on 31/03/2021.
//
import SwiftUI
import CoreData
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
var body: some View {
VStack {
Button {
contentViewModel.addItem()
} label: {
Label("Add Item", systemImage: "plus")
}
if !contentViewModel.items.isEmpty {
List {
ForEach(contentViewModel.items) { item in
if let tt = item.timestamp {
Text("Item at \(tt, formatter: itemFormatter)")
} else {
Text("No item timestamp")
}
}
}
}
}
}
}
class ContentViewModel: ObservableObject {
private let coreDataStack = PersistenceController.shared
#Published var items : [Item] = []
init() {
getItems()
}
func getItems() {
let fetchItemsCompletion : (Result<[Item], Error>) -> Void = { result in
switch result {
case .success(let fetchedItems):
self.items = fetchedItems
case .failure( _):
NSLog("fetchAllConsumption failure");
}
}
fetchAllItemFromCoreData(completion: fetchItemsCompletion)
}
func addItem() {
let addItemCompletion : (Result<Item, Error>) -> Void = { result in
switch result {
case .success(let fetchedItem):
self.items.insert(fetchedItem, at: 0)
case .failure( _):
NSLog("fetchAllConsumption failure");
}
}
addItemToCoreData(completion: addItemCompletion)
}
func addItemToCoreData(completion: #escaping (Result<Item, Error>) -> Void) {
coreDataStack.container.performBackgroundTask { context in //its not working
let newItem = Item(context: context)
newItem.timestamp = Date()
do {
try context.save()
completion(.success(newItem))
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
func fetchAllItemFromCoreData(completion: #escaping (Result<[Item], Error>) -> Void) {
coreDataStack.container.performBackgroundTask { context in //its not working
do {
let teaRequest: NSFetchRequest<Item> = Item.fetchRequest()
let result = try context.fetch(teaRequest)
completion(.success(result))
}
catch {
completion(.failure(error))
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
If I add items, I get [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates. error message.
In the fetch, there is no timestamp visible on the list.
It's work well if I fetch data in main thread with the viewContext of NSPersistentContainer.
I've generated a lot of data to test, here you can download:
https://www.dropbox.com/s/0xqhn0b4mtzumz1/FetchInBackground_sqlite.zip
After the first run in the simulator you can find the created sqlite with this command in terminal:
find /Users/$USER/Library/Developer/CoreSimulator/Devices -iname FetchInBackground.sqlite
Now you can replace it with the files found in the zip.
Thank you very much in advance for your help!

SwiftUI app freezing when using multiple product identifiers in StoreKit

I'm currently learning Swift and following some tutorials but I'm stuck on a StoreKit issue.
The code works when I provide a single productIdentifier, but when I provide more than 1 in the Set, the entire app hangs on loading. This is in the iOS Simulator, and on a device. I've got 2 identifiers in the set, and both of these work individually, but not at the same time. My code looks the same as the original tutorial (video) so I don't know where I'm going long.
Entire Store.swift file below. Problem appears to be in the fetchProducts function, but I'm not sure. Can anyone point me in the right direction?
import StoreKit
typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)
class Store: NSObject, ObservableObject {
#Published var allRecipes = [Recipe]() {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
for index in self.allRecipes.indices {
self.allRecipes[index].isLocked = !self.completedPurchases.contains(self.allRecipes[index].id)
}
}
}
}
private let allProductIdentifiers = Set(["com.myname.ReceipeStore.test", "com.myname.ReceipeStore.test2"])
private var completedPurchases = [String]()
private var productsRequest: SKProductsRequest?
private var fetchedProducts = [SKProduct]()
private var fetchCompletionHandler: FetchCompletionHandler?
private var purchaseCompletionHandler: PurchaseCompletionHandler?
override init() {
super.init()
startObservingPaymentQueue()
fetchProducts { products in
self.allRecipes = products.map { Recipe(product: $0) }
}
}
private func startObservingPaymentQueue() {
SKPaymentQueue.default().add(self)
}
private func fetchProducts(_ completion: #escaping FetchCompletionHandler) {
guard self.productsRequest == nil else { return }
fetchCompletionHandler = completion
productsRequest = SKProductsRequest(productIdentifiers: allProductIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
private func buy(_ product: SKProduct, competion: #escaping PurchaseCompletionHandler) {
purchaseCompletionHandler = competion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
}
extension Store {
func product(for identififier: String) -> SKProduct? {
return fetchedProducts.first(where: { $0.productIdentifier == identififier })
}
func purchaseProduct(_ product: SKProduct) {
buy(product) { _ in }
}
}
extension Store: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
var shouldFinishTransactions = false
switch transaction.transactionState {
case .purchased, .restored:
completedPurchases.append(transaction.payment.productIdentifier)
shouldFinishTransactions = true
case .failed:
shouldFinishTransactions = true
case .deferred, .purchasing:
break
#unknown default:
break
}
if shouldFinishTransactions {
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async {
self.purchaseCompletionHandler?(transaction)
self.purchaseCompletionHandler = nil
}
}
}
}
}
// loading products from the store
extension Store: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let loadedProducts = response.products
let invalidProducts = response.invalidProductIdentifiers
guard !loadedProducts.isEmpty else {
print("Could not load the products!")
if !invalidProducts.isEmpty {
print("Invalid products found: \(invalidProducts)")
}
productsRequest = nil
return
}
// cache the feteched products
fetchedProducts = loadedProducts
// notify anyone waiting on the product load (swift UI view)
DispatchQueue.main.async {
self.fetchCompletionHandler?(loadedProducts)
self.fetchCompletionHandler = nil
self.productsRequest = nil
}
}
}```
It looks like you're running all of your requests on the main DispatchQueue, this will block other main queue work until completed. You should consider handling some of these tasks with a custom concurrent queue. This bit of sample code should get the ball rolling.
func requestProducts(_ productIdentifiers: Set<ProductIdentifier>, handler: #escaping ProductRequestHandler) {
// Set request handler
productRequest?.cancel()
productRequestHandler = handler
// Request
productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest?.delegate = self
productRequest?.start()
}
func requestPrices() {
// Retry interval, 5 seconds, set this to your liking
let retryTimeOut = 5.0
var local1: String? = nil
var local2: String? = nil
let bundleIdentifier = Bundle.main.bundleIdentifier!
let queue = DispatchQueue(label: bundleIdentifier + ".IAPQueue", attributes: .concurrent)
// Request price
queue.async {
var trying = true
while(trying) {
let semaphore = DispatchSemaphore(value: 0)
requestProducts(Set(arrayLiteral: SettingsViewController.pID_1000Credits, SettingsViewController.pID_2000Credits)) { (response, error) in
local1 = response?.products[0].localizedPrice
local2 = response?.products[1].localizedPrice
semaphore.signal()
}
// We will keep checking on this thread until completed
_ = semaphore.wait(timeout: .now() + retryTimeOut)
if(local2 != nil) { trying = false }
}
// Update with main thread once request is completed
DispatchQueue.main.async {
self.price1 = local1 ?? "$0.99"
self.price2 = local2 ?? "$1.99"
}
}
}
extension SKProduct {
// Helper function, not needed for this example
public var localizedPrice: String? {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = self.priceLocale
return formatter.string(from: self.price)
}

Write unit tests for ObservableObject ViewModels with Published results

Today again one combine problem I currently run in and I hope that someone of you can help. How can normal unit tests be written for ObservableObjects classes which contain #Published attributes? How can I subscribe in my test to them to get the result object which I can assert?
The injected mock for the web service works correctly, loadProducts() function set exactly the same elements from the mock in the fetchedProducts array.
But I don't know currently how to access this array in my test after it is filled by the function because it seems that I cannot work with expectations here, loadProducts() has no completion block.
The code looks like this:
class ProductsListViewModel: ObservableObject {
let getRequests: GetRequests
let urlService: ApiUrls
private let networkUtils: NetworkRequestUtils
let productsWillChange = ObservableObjectPublisher()
#Published var fetchedProducts = [ProductDTO]()
#Published var errorCodeLoadProducts: Int?
init(getRequestsHelper: GetRequests, urlServiceClass: ApiUrls = ApiUrls(), utilsNetwork: NetworkRequestUtils = NetworkRequestUtils()) {
getRequests = getRequestsHelper
urlService = urlServiceClass
networkUtils = utilsNetwork
}
// nor completion block in the function used
func loadProducts() {
let urlForRequest = urlService.loadProductsUrl()
getRequests.getJsonData(url: urlForRequest) { [weak self] (result: Result<[ProductDTO], Error>) in
self?.isLoading = false
switch result {
case .success(let productsArray):
// the products filled async here
self?.fetchedProducts = productsArray
self?.errorCodeLoadProducts = nil
case .failure(let error):
let errorCode = self?.networkUtils.errorCodeFrom(error: error)
self?.errorCodeLoadProducts = errorCode
print("error: \(error)")
}
}
}
}
The test I try to write looks like this at the moment:
import XCTest
#testable import MyProject
class ProductsListViewModelTest: XCTestCase {
var getRequestMock: GetRequests!
let requestManagerMock = RequestManagerMockLoadProducts()
var productListViewModel: ProductsListViewModel!
override func setUp() {
super.setUp()
getRequestMock = GetRequests(networkHelper: requestManagerMock)
productListViewModel = ProductsListViewModel(getRequestsHelper: getRequestMock)
}
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
productListViewModel.loadProducts()
// TODO access the fetchedProducts here somehow and assert them
}
}
The Mock looks like this:
class RequestManagerMockLoadProducts: NetworkRequestManagerProtocol {
var isSuccess = true
func makeNetworkRequest<T>(urlRequestObject: URLRequest, completion: #escaping (Result<T, Error>) -> Void) where T : Decodable {
if isSuccess {
let successResultDto = returnedProductedArray() as! T
completion(.success(successResultDto))
} else {
let errorString = "Cannot create request object here"
let error = NSError(domain: ErrorDomainDescription.networkRequestDomain.rawValue, code: ErrorDomainCode.unexpectedResponseFromAPI.rawValue, userInfo: [NSLocalizedDescriptionKey: errorString])
completion(.failure(error))
}
}
func returnedProductedArray() -> [ProductDTO] {
let product1 = ProductDTO(idFromBackend: "product-1", name: "product-1", description: "product-description", price: 3.55, photo: nil)
let product2 = ProductDTO(idFromBackend: "product-2", name: "product-2", description: "product-description-2", price: 5.55, photo: nil)
let product3 = ProductDTO(idFromBackend: "product-3", name: "product-3", description: "product-description-3", price: 8.55, photo: nil)
return [product1, product2, product3]
}
}
Maybe this article can help you
Testing your Combine Publishers
To solve your issue I will use code from my article
typealias CompetionResult = (expectation: XCTestExpectation,
cancellable: AnyCancellable)
func expectValue<T: Publisher>(of publisher: T,
timeout: TimeInterval = 2,
file: StaticString = #file,
line: UInt = #line,
equals: [(T.Output) -> Bool])
-> CompetionResult {
let exp = expectation(description: "Correct values of " + String(describing: publisher))
var mutableEquals = equals
let cancellable = publisher
.sink(receiveCompletion: { _ in },
receiveValue: { value in
if mutableEquals.first?(value) ?? false {
_ = mutableEquals.remove(at: 0)
if mutableEquals.isEmpty {
exp.fulfill()
}
}
})
return (exp, cancellable)
}
your test needs to use this function
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
/// The expectation here can be extended as needed
let exp = expectValue(of: productListViewModel .$fetchedProducts.eraseToAnyPublisher(), equals: [{ $0[0].idFromBackend == "product-1" }])
productListViewModel.loadProducts()
wait(for: [exp.expectation], timeout: 1)
}
The easy and clearest way for me is simply to test #published var after X seconds. An example bellow :
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
productListViewModel.loadProducts()
// TODO access the fetchedProducts here somehow and assert them
let expectation = XCTestExpectation()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
XCTAssertEqual(self.productListViewModel.fetchedProducts, ["Awaited values"])
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
I hope that helps !

Failed to demangle witness error when using Realm in a custom Publisher

I've never come across a witness table error before but this is my first venture into testing custom Publishers and, if I was to guess, I suspect there is something weird and wonderful going on with threading based on how mangled the witness name is. Completely out at sea here so a pointer (or pointers!) would be much appreciated.
Custom publisher
// MARK: Custom publisher - produces a stream of Object arrays in response to change notifcations on a given Realm collection
extension Publishers {
struct Realm<Collection: RealmCollection>: Publisher {
typealias Output = Array<Collection.Element>
typealias Failure = Never // TODO: Not true but deal with this later
let collection: Collection
init(collection: Collection) {
self.collection = collection
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = RealmSubscription(subscriber: subscriber, collection: collection)
subscriber.receive(subscription: subscription)
}
}
}
// MARK: Convenience accessor function to the custom publisher
extension Publishers {
static func realm<Collection: RealmCollection>(collection: Collection) -> Publishers.Realm<Collection> {
return Publishers.Realm(collection: collection)
}
}
// MARK: Custom subscription
private final class RealmSubscription<S: Subscriber, Collection: RealmCollection>: Subscription where S.Input == Array<Collection.Element> {
private var subscriber: S?
private let collection: Collection
private var notificationToken: NotificationToken?
init(subscriber: S, collection: Collection) {
self.subscriber = subscriber
self.collection = collection
self.notificationToken = collection.observe { (changes: RealmCollectionChange) in
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
print("Initial")
let _ = subscriber.receive(Array(collection.elements)) // ERROR THROWN HERE
// case .update(_, let deletions, let insertions, let modifications):
case .update(_, _, _, _):
print("Updated")
let _ = subscriber.receive(Array(collection.elements))
case .error(let error):
fatalError("\(error)")
#warning("Impl error handling - do we want to fail or log and recover?")
}
}
}
func request(_ demand: Subscribers.Demand) {
// no impl as RealmSubscriber is effectively just a sink
}
func cancel() {
print("Cancel called on RealnSubscription")
subscriber = nil
notificationToken = nil
}
}
Service class
protocol RealmServiceType {
func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object
#discardableResult
func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never>
func deletePatient(_ patient: Patient, from realm: Realm)
}
extension RealmServiceType {
func all<Element>(_ type: Element.Type) -> AnyPublisher<Array<Element>, Never> where Element: Object {
print("Called \(#function)")
return all(type, within: try! Realm())
}
}
final class TestRealmService: RealmServiceType {
private let patients = [
Patient(name: "Tiddles"), Patient(name: "Fang"), Patient(name: "Phoebe"), Patient(name: "Snowy")
]
init() {
let realm = try! Realm()
guard realm.isEmpty else { return }
try! realm.write {
for p in patients {
realm.add(p)
}
}
}
func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object {
return Publishers.realm(collection: realm.objects(type).sorted(byKeyPath: "name")).eraseToAnyPublisher()
}
func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never> {
let patient = Patient(name: name)
try! realm.write {
realm.add(patient)
}
return Just(patient).eraseToAnyPublisher()
}
func deletePatient(_ patient: Patient, from realm: Realm) {
try! realm.write {
realm.delete(patient)
}
}
}
Test case
class AthenaVSTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
private var service: RealmServiceType?
override func setUp() {
service = TestRealmService()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
service = nil
cancellables.removeAll()
}
func testRealmPublisher() {
var outcome = [""]
let expectation = self.expectation(description: #function)
let expected = ["Tiddles", "Fang", "Phoebe", "Snowy"]
let _ = service?.all(Patient.self)
.sink(receiveCompletion: { _ in
expectation.fulfill() },
receiveValue: { value in
outcome += value.map { $0.name }
})
.store(in: &cancellables)
waitForExpectations(timeout: 2, handler: nil)
XCTAssert(outcome == expected, "Expected \(expected) Objects but got \(outcome)")
}
}
Error message
failed to demangle witness for associated type 'Iterator' in conformance 'RealmSwift.Results: Sequence' from mangled name '10RealmSwift11RLMIteratorVyxG'
2020-01-13 22:46:07.159964+0000 AthenaVS[3423:171342] failed to demangle witness for associated type 'Iterator' in conformance 'RealmSwift.Results: Sequence' from mangled name '10RealmSwift11RLMIteratorVyxG'
The error is thrown when attempting to execute code in the Realm notification observer within RealmSubscription (I've flagged it in the code above), specifically:
let _ = subscriber.receive(Array(collection.elements))
Ideas?
This was a red herring, but at all relating to Combine but rather some Swift build issue, try switching to Carthage instead of using SPM to see if the problem goes away.
For other this link might be relevant

RxSwift and MVVM: observable not executing without binding

I'm new to RxSwift and trying implement app that using MVVM architecture. I have view model:
class CategoriesViewModel {
fileprivate let api: APIService
fileprivate let database: DatabaseService
let categories: Results<Category>
// Input
let actionRequest = PublishSubject<Void>()
// Output
let changeset: Observable<(AnyRealmCollection<Category>, RealmChangeset?)>
let apiSuccess: Observable<Void>
let apiFailure: Observable<Error>
init(api: APIService, database: DatabaseService) {
self.api = api
self.database = database
categories = database.realm.objects(Category.self).sorted(byKeyPath: Category.KeyPath.name)
changeset = Observable.changeset(from: categories)
let requestResult = actionRequest
.flatMapLatest { [weak api] _ -> Observable<Event<[Category]>> in
guard let strongAPI = api else {
return Observable.empty()
}
let request = APIService.MappableRequest(Category.self, resource: .categories)
return strongAPI.mappedArrayObservable(from: request).materialize()
}
.shareReplayLatestWhileConnected()
apiSuccess = requestResult
.map { $0.element }
.filterNil()
.flatMapLatest { [weak database] newObjects -> Observable<Void> in
guard let strongDatabase = database else {
return Observable.empty()
}
return strongDatabase.updateObservable(with: newObjects)
}
apiFailure = requestResult
.map { $0.error }
.filterNil()
}
}
and I have following binginds in view controller:
viewModel.apiSuccess
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.apiFailure
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
But if I comment bindings, part with database updating stops executing. I need to make it execute anyway, without using dispose bag in the view model. Is it possible?
And little additional question: should I use weak-strong dance with api/database and return Observable.empty() like in my view model code or can I just use unowned api/unowned database safely?
Thanks.
UPD:
Function for return observable in APIService:
func mappedArrayObservable<T>(from request: MappableRequest<T>) -> Observable<[T]> {
let jsonArray = SessionManager.jsonArrayObservable(with: request.urlRequest, isSecured: request.isSecured)
return jsonArray.mapResponse(on: mappingSheduler, { Mapper<T>().mapArray(JSONArray: $0) })
}
Work doesn't get done unless there is a subscriber prepared to receive the results.
Your DatabaseService needs to have a dispose bag in it and subscribe to the Observable<[Category]>. Something like:
class ProductionDatabase: DatabaseService {
var categoriesUpdated: Observable<Void> { return _categories }
func updateObservable(with categories: Observable<[Category]>) {
categories
.subscribe(onNext: { [weak self] categories in
// store categories an then
self?._categories.onNext()
})
.disposed(by: bag)
}
private let _categories = PublishSubject<Void>()
private let bag = DisposeBag()
}
Then apiSuccess = database.categoriesUpdated and database.updateObservable(with: requestResult.map { $0.element }.filterNil())