RxSwift onNext not calling scan - ios

I am trying to create a UICollectionView, so that I can add and delete items from it's data source as a Driver. I have a viewModel below
import Photos
import RxCocoa
import RxSwift
class GroupedAssetsViewModel {
enum ItemAction {
case add(item: PHAssetGroup)
case remove(indexPaths: [IndexPath])
}
let assets: Driver<[GroupedAssetSectionModel]>
let actions = PublishSubject<ItemAction>()
private let deletionService: AssetDeletionService = AssetDeletionServiceImpl()
init() {
assets = actions
.debug()
.scan(into: []) { [deletionService] items, action in
switch action {
case .add(let item):
let model = GroupedAssetSectionModel()
items.append(GroupedAssetSectionModel(original: model, items: item.assets))
case .remove(let indexPaths):
var assets = [PHAsset]()
for indexPath in indexPaths {
items[indexPath.section].items.remove(at: indexPath.item)
assets.append(items[indexPath.section].items[indexPath.row])
}
deletionService.delete(assets: assets)
}
}
.asDriver(onErrorJustReturn: [])
}
func setup(with assetArray: [PHAssetGroup] = [PHAssetGroup]()) {
for group in assetArray {
actions.onNext(.add(item: group))
}
}
}
but .scan closure is never being called, even though actions.onNext is being called in setup, therefore Driver's value is always empty.
It seems like I am getting some core concepts wrong, what might be the problem here?

Just because you have
actions.onNext(.add(item: group)) doesn't mean this sequence has started. You are publishing events to a subject that hasn't started. You must have a subscriber somewhere first for assets. Then only scan will get executed. Because observables are pull driven sequences. There must be a subscriber to even make them start.

Related

rxswift viewmodel with input output

I am trying to achieve something similar in rxswift example project from RxSwift repo. But in my case there are dependent observables. I couldn't find any solution without using binding in viewmodel
Here is the structure of my viewmodel:
First the definitions of input, output and viewmodel
typealias UserListViewModelInput = (
viewAppearAction: Observable<Void>,
deleteAction: Observable<Int>
)
typealias UserListViewModelOutput = Driver<[User]>
typealias UserListViewModel = (UserListViewModelInput, #escaping UserApi) -> UserListViewModelOutput
Then there is actual implementation which doesn't compile.
let userListViewModel: UserListViewModel = { input, loadUsers in
let loadedUserList = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.elements()
.asDriver(onErrorDriveWith: .never())
let userListAfterDelete = input.deleteAction
.withLatestFrom(userList) { index, users in
users.enumerated().compactMap { $0.offset != index ? $0.element : nil }
}
.asDriver(onErrorJustReturn: [])
let userList = Driver.merge([loadedUserList, userListAfterDelete])
return userList
}
Viewmodel has two job. First load the user list. Second is delete a user at index. The final output is the user list which is downloaded with UserApi minus deleted users.
The problem in here in order the define userList I need to define userListAfterDelete. And in order to define userListAfterDelete I need to define userList.
So is there a way to break this cycle without using binding inside view model? Like a placeholder observable or operator that keeps state?
This is a job for a state machine. What you will see in the code below is that there are two actions that can affect the User array. When the view appears, a new array is downloaded, when delete comes in, a particular user is removed.
This is likely the most common pattern seen in reactive code dealing with state. So common that there are whole libraries that implement some variation of it.
let userListViewModel: UserListViewModel = { input, loadUsers in
enum Action {
case reset([User])
case delete(at: Int)
}
let resetUsers = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.compactMap { $0.element }
.map { Action.reset($0) }
let delete = input.deleteAction.map { Action.delete(at: $0) }
return Observable.merge(resetUsers, delete)
.scan(into: [User](), accumulator: { users, action in
switch action {
case let .reset(newUsers):
users = newUsers
case let .delete(index):
users.remove(at: index)
}
})
.asDriver(onErrorJustReturn: [])
}

Synchronous processing with DispatchQueue in Swift

I have a method which does processing for the events I receive from the server. The method can be called from multiple places in different classes. I want to synchronize the processing of the events using DispatchQueue/Serial Queue to discard the duplicate events in multiple calls. I know about dispatch queues and how it works but I am unable to find the best solution for my problem.
To achieve: By synchronizing I want to ensure sequential processing, to discard duplicate events.
func process(events:[Events]) {
// by synchronizing I want to ensure sequential processing, to discard duplicate events
for event in events {
// process, save to db,
}
// issue notifications, etc
}
class A {
process(events)
}
class B {
process(events)
}
Any help is appreciated. Thanks!
Try something like this:
class Event {
let id: String = ""
}
class EventManager {
static let shared = EventManager()
private let processQueue = DispatchQueue(label: "processQueue")
private var processedEvents = [Event]()
private init() {}
func process(events:[Event]) {
processQueue.async { [unowned self] in
for event in events {
if !self.processedEvents.contains(where: { $0.id == event.id }) {
// process, save to db,
self.processedEvents.append(event)
}
}
// issue notifications, etc
}
}
}

Automatic UI updates with Apollo in Swift not working

I have the following setup for a small Apollo iOS app where I display a list of conferences in a table view and want to be able to add a conference to the list:
GraphQL:
query AllConferences {
allConferences {
...ConferenceDetails
}
}
mutation CreateConference($name: String!, $city: String!, $year: String!) {
createConference(name: $name, city: $city, year: $year) {
...ConferenceDetails
}
}
fragment ConferenceDetails on Conference {
id
name
city
year
attendees {
...AttendeeDetails
}
}
fragment AttendeeDetails on Attendee {
id
name
conferences {
id
}
}
ConferencesTableViewController:
class ConferencesTableViewController: UITableViewController {
var allConferencesWatcher: GraphQLQueryWatcher<AllConferencesQuery>?
var conferences: [ConferenceDetails] = [] {
didSet {
tableView.reloadData()
}
}
deinit {
allConferencesWatcher?.cancel()
}
override func viewDidLoad() {
super.viewDidLoad()
allConferencesWatcher = apollo.watch(query: AllConferencesQuery()) { result, error in
print("Updating conferences: ", result?.data?.allConferences)
guard let conferences = result?.data?.allConferences else {
return
}
self.conferences = conferences.map { $0.fragments.conferenceDetails }
}
}
// ...
// standard implementation of UITableViewDelegate
// ...
}
AddConferenceViewController:
class AddConferenceViewController: UIViewController {
// ... IBOutlets
#IBAction func saveButtonPressed() {
let name = nameTextField.text!
let city = cityTextField.text!
let year = yearTextField.text!
apollo.perform(mutation: CreateConferenceMutation(name: name, city: city, year: year)) { result, error in
if let _ = result?.data?.createConference {
self.presentingViewController?.dismiss(animated: true)
}
}
}
}
I also implemented cacheKeyForObject in AppDelegate like so:
apollo.cacheKeyForObject = { $0["id"] }
My question is whether it is possible to benefit from automatic UI updates with this setup? Currently when the CreateConferenceMutation is performed, the table view is not updated. Am I missing something or am I hitting the limitation that is mentioned in the docs:
In some cases, just using cacheKeyFromObject is not enough for your application UI to update correctly. For example, if you want to add something to a list of objects without refetching the entire list, or if there are some objects that to which you can’t assign an object identifier, Apollo cannot automatically update existing queries for you.
This is indeed a limitation of automatic UI updates. Although Apollo uses cacheKeyFromObject to match objects by ID, and this covers many common cases, it can't automatically update lists of objects.
In your schema, there is no way for Apollo to know that a newly added conference should be added to the allConferences list. All it knows is that allConferences returns a list of conference objects, but these could be arbitrarily selected and ordered.
So in cases like these, you will have to refetch the query from the server yourself, or change the mutation result to include the updated list.
Another option would be to manually add the new conference to the list in the client store. For this, the next version of Apollo iOS will include a manual update option similar to updateQueries in the Apollo JavaScript client.

Mapping JSON response to objects using Rx programming (Moya)

I am currently trying to learn Rx programming. I found Moya intriguing and have been trying to implement a simple network request which then gets mapped to objects which I can then use to populate a tableView.
I have been following this tutorial: http://www.thedroidsonroids.com/blog/ios/rxswift-examples-3-networking/
I believe I am getting a successful response as I am using .debug and getting the following output:
2016-04-09 13:29:30.398: MyApi.swift:37 (findRepository) -> subscribed
2016-04-09 13:29:30.400: MyApi.swift:35 (findRepository) -> subscribed
2016-04-09 13:29:32.633: MyApi.swift:35 (findRepository) -> Event Next(Status Code: 20..., Data Length: 5747)
2016-04-09 13:29:32.633: MyApi.swift:35 (findRepository) -> Event Completed
Here is the code that I am using:
let provider: RxMoyaProvider<MyApi>
let repositoryName: Observable<String>
func trackIssues() -> Observable<[Train]> {
return repositoryName
.observeOn(MainScheduler.instance)
.flatMapLatest { name -> Observable<[Train]?> in
print("Name: \(name)")
return self.findRepository(name)
}.replaceNilWith([])
}
internal func findRepository(name: String) -> Observable<[Train]?> {
print("help")
return self.provider
.request(MyApi.Trains(name, "c76a46ce2b3d8685982b/raw/10e86080c3b1beedd46db47f5bb188cc74ce5c78/sample.json"))
.debug()
.mapArrayOptional(Train.self)
.debug()
}
And here is the object I am trying to map to:
import Mapper
struct Train: Mappable {
let distance: String
let eta: String
init(map: Mapper) throws {
try distance = map.from("distance")
try eta = map.from("eta")
}
}
I have looked at the network response and am wondering if I first need to abstract the "trains" data. I have tried this by mapping to the following object with out luck:
import Mapper
struct TrainsResponse: Mappable {
let trains: String
init(map: Mapper) throws {
try trains = map.from("trains")
}
}
Please find example json response here: http://pastebin.com/Wvx8d5Lg
So I was wondering if anyone can help me see why I am unable to turn the response into objects. Thanks.
=======
I have tried doing a pod update and it's still not working. Here is where I am binding it to the tableView:
func setupRx() {
// First part of the puzzle, create our Provider
provider = RxMoyaProvider<MyApi>()
// Now we will setup our model
myApi = MyApi(provider: provider, repositoryName: userName)
// And bind issues to table view
// Here is where the magic happens, with only one binding
// we have filled up about 3 table view data source methods
myApi
.trackIssues()
.bindTo(tableView.rx_itemsWithCellFactory) { (tableView, row, item) in
let cell = tableView.dequeueReusableCellWithIdentifier("issueCell", forIndexPath: NSIndexPath(forRow: row, inSection: 0))
cell.textLabel?.text = "Hello"
print("Hello")
return cell
}
.addDisposableTo(disposeBag)
}
The code inside bind to (where I set the cell) never gets called. Also if I put a break point inside my Train mapper class that also never gets called.
The reason it doesn't work is that the JSON retrieved from the server returns dictionary, not an array. The array you wanted to parse is under the "trains" key in that dictionary. The Moya-ModelMapper used in the tutorial has methods for this usage as well. You just need to pass second argument, keyPath: in a mapArrayOptional() method.
So the answer for your question is replace:
.mapArrayOptional(Train.self)
with
.mapArrayOptional(Train.self, keyPath: "trains")

Realm notification to RX block

I would like to hide my Realm implementation and instead of working on RLMNotificationBlock I would like to use RXSwift. Below how my method looks like now (RLMNotificationBlock is a block that takes String and RLMRealm):
func addNotificationBlock(block: RLMNotificationBlock) -> RLMNotificationToken? {
let rlmObject = ...
return rlmObject.addNotificationBlock(block)
}
But I would like to switch to more reactive observer-pattern way. I looked at RxSwift docs and source code of rx_clickedButtonAtIndex, but I cannot figure out how I should put all these things together. I guess my code at the end would look like:
public var rx_realmContentChanged: ControlEvent<Int> {
let controlEvent = ControlEvent()
// My code go here
return controlEvent
}
I'm new with RXSwift and know only the basics. Any help will be appreciated.
There is an Rx Realm extension available on GitHub you can use: https://github.com/RxSwiftCommunity/RxRealm
It allows you to get an Observable out of a single Realm object or a Realm Collection. Here's an example from the README:
let realm = try! Realm()
let laps = realm.objects(Lap.self))
Observable.changesetFrom(laps)
.subscribe(onNext: { results, changes in
if let changes = changes {
// it's an update
print(results)
print("deleted: \(changes.deleted) inserted: \(changes.inserted) updated: \(changes.updated)")
} else {
// it's the initial data
print(results)
}
})
There is also an additional library especially built for binding table and collection views called RxRealmDataSources
If I understood you correctly, you just want to return Observable<RLMNotificationToken>
In this case you just need to do something like this
func addNotificationBlock(block: RLMNotificationBlock) -> Observable<RLMNotificationToken> {
return create { observer -> Disposable in
let rlmObject = ...
let token = rlmObject.addNotificationBlock(block)
// Some condition
observer.onNext(token)
// Some other condition
observer.onError(NSError(domain: "My domain", code: -1, userInfo: nil))
return AnonymousDisposable {
// Dispose resources here
}
// If you have nothing to dipose return `NopDisposable.instance`
}
}
In order to use it bind it to button rx_tap or other use flatMap operator

Resources