RxSwift and MVVM: observable not executing without binding - swift

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

Related

Swift - Combine subscription not being called

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

How do I bind a ViewModel to a Collationview?

I'm trying to bind a view model to a collection view. But I don't know how to do it. I'm using MVVM pattern and RxSwift, and I've only tried table view binding before. Here's my view model and the view controller code I've done so far.
class SearchViewModel: ViewModelType {
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
This is my ViewModel code
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
}
And this is my ViewController code.
How should the collection view bind?
Here is what your view model should look like:
class SearchViewModel {
// no need for a disposedBag. If you are putting a disposeBag in your view model, you are likely doing something wrong.
struct input {
let loadData: Signal<Void>
}
struct output {
let loadApplyList: Driver<[User]> // you should be passing an array here, not an object.
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let friendResult = input.loadData
.flatMapLatest {
api.getFriend()
.compactMap { $0.0.map(Result<friends, Error>.success) }
.asDriver(onErrorRecover: { Driver.just(Result<friends, Error>.failure($0)) })
}
let loadApplyList = friendResult
.compactMap { (result) -> [User]? in
guard case let .success(list) = result else { return nil }
return list.friends
}
return output(loadApplyList: loadApplyList)
}
}
Now in your view controller, you can bind it like this:
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList
.drive(collectionView.rx.items(cellIdentifier: "Cell", cellType: MyCellType.self)) { index, item, cell in
// configure cell with item here
}
.disposed(by: disposeBag)
}

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

How to use RxDataSource with SearchBar?

I master RxSwift and when using RxDataSource, the SearchBar delegates do not work for me and he,
I can’t see the error. Without RxDataSource everything works, on other screens I have no problems.
Tell me, with a fresh look, what is the mistake? why doesn't the filter happen?
private var defaultCategories: [Groups]!
var groupsCoreData = BehaviorRelay<[Groups]>(value: [])
override func viewDidLoad() {
super.viewDidLoad()
searchBarRx()
tableViewRx()
}
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Groups>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "addNewWordsToGroup")!
cell.textLabel?.text = element.title
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)
private func tableViewRx() {
let dataSource = self.dataSource
let items = [
SectionModel(model: "Пример", items: self.defaultCategories
.filter { $0.titleCategories == "Тест1"}),
SectionModel(model: "Пример2", items: self.defaultCategories
.filter { $0.titleCategories == "Тест2" })
]
Observable.just(items)
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
tableView
.rx
.modelSelected(Groups.self)
.subscribe(onNext: { [weak self] data in
}
.disposed(by: disposeBag)
}
private func searchBarRx() {
searchBar
.rx
.text
.orEmpty
.debounce(.microseconds(200), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe { [unowned self] query in
self.searchBar.showsCancelButton = query.element!.isEmpty
self.defaultCategories = query.element!.isEmpty ?
self.defaultCategories :
self.defaultCategories
.filter({ $0.title?.range(of: query.element!, options: .anchored) != nil
})
}
.disposed(by: disposeBag)
}
query - displays the input characters, but no result.
P.S. the arrays are not empty
The key is that you don't replace the datasource. Rx is a functional paradigm so no replacement is required. Instead you have to outline your invariants before hand. Like so:
final class ViewController: UIViewController {
var tableView: UITableView!
var searchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let initialItems = [
SectionModel(model: "Пример", items: [Groups(title: "Group1", titleCategories: "Тест1")]),
SectionModel(model: "Пример2", items: [Groups(title: "Group2", titleCategories: "Тест2")])
]
let searchTerm = searchBar.rx.text.orEmpty
.debounce(.microseconds(200), scheduler: MainScheduler.instance)
.distinctUntilChanged()
Observable.combineLatest(Observable.just(initialItems), searchTerm)
.map { filteredSectionModels(sectionModels: $0.0, filter: $0.1) }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
func filteredSectionModels(sectionModels: [SectionModel<String, Groups>], filter: String) -> [SectionModel<String, Groups>] {
guard !filter.isEmpty else { return sectionModels }
return sectionModels.map {
SectionModel(model: $0.model, items: $0.items.filter { $0.title?.range(of: filter, options: .anchored) != nil
})
}
}
private let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Groups>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "addNewWordsToGroup")!
cell.textLabel?.text = element.title
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)
Pay special attention to how I combined the Observable that contains all the items with the Observable that tracks the current search filter. Then I only send the items to the table view that are actually supposed to be displayed.

RxSwift withLatestFrom with resultSelector doesn't compile

I have a Driver of type Bool and a BehaviorRelay of type Page (which is a custom enum).
enum Page {
case option1(CustomClass1, CustomClass2)
case option2(CustomClass3)
case option3(CustomClass4)
var property1: CustomClass2? {
switch self {
case .option1(_, let custom):
return custom
case .option2, .option3:
return nil
}
}
}
I have the Driver<Bool> in another ViewModel.
class ViewModel1 {
struct Output {
let hasItems: Driver<Bool>
}
let output: Output
init() {
let hasItemsRelay: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)
self.output = Output(
hasItems: hasItemsRelay.asDriver()
)
}
}
And I have a BehaviorRelay<Page?> in my base class.
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
init() {
self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
}
In ViewModel2 class I'm trying to catch an event on the hasItems driver of ViewModel1.Input and when I get an event, I need the current value of currentPageRelay and later on do stuff with it. So basically withLatestFrom is the thing I need to use.
class ViewModel2 {
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.unwrap()
.drive(onNext: { (property1, hasItems) in
// do stuff
}
.disposed(by: disposeBag)
}
}
Xcode completely loses it on the withLatestFrom. No code completion and it gives the following compile error:
Expression type '(Bool, _)' is ambiguous without more context
I'm completely in the dark about this one. I've already tried everything, providing the correct classes in the parameter list below it, so that it knows what to expect etc, but no luck so far.
I think because .withLatestFrom requires both types it operates on to be of the same observable trait. So both should be either Observable, Driver, Signal, etc.
If you want to keep your Driver in your viewModel a Driver you could add an .asObservable() after the .hasItems:
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
let disposeBag = DisposeBag()
init() {
// self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.asObservable()
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.asDriver(onErrorJustReturn: nil)
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)
}
}
Or add a .asDriver() to currentPageRelay in the withLatestFrom(..):
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay.asDriver()) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)