Synchronous processing with DispatchQueue in Swift - ios

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

Related

Custom Combine Publisher for a single database listener

I have an iOS app with a custom database, To retrieve data from my database I setup a listener like this:
var listener: DatabaseListener?
self.listener = db.items.addListener { items in
}
// later when I don't need the listener any more:
self.listener?.cancel()
This listener gives the items as soon as I set it up and notifies me whenever my data is updated, It also stays alive until I manually cancel it. I also store a cache of the retrieved items in UserDefaults to speed things up (See it in action in the example bellow).
Now I'm trying to start using Combine to retrieve my items, I want to setup the database listener as soon as a new subscription is created (for example when sink or assign are called) and cancel it when there's no more subscriptions left.
So here's what I came up with:
class ItemsSubscription: Subscription {
private var subscriber: (any Subscriber<[Item], Never>)?
private var listener: DatabaseListener?
private var items: [Item] = UserDefaults.standard.cacheItems
init(subscriber: any Subscriber<[Item], Never>) {
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
let _ = subscriber?.receive(items)
self.listener = db.items.addListener {
UserDefaults.standard.cacheItems = $0
self.items = $0
let _ = self.subscriber?.receive($0)
}
}
func cancel() {
self.listener?.cancel()
self.listener = nil
self.subscriber = nil
}
}
struct ItemsPublisher: Publisher {
typealias Output = [Item]
typealias Failure = Never
func receive<S>(subscriber: S) where S: Subscriber, S.Input == [Item], S.Failure == Never {
let subscription = ItemsSubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
Then I'm using ItemsPublisher like this:
private var cancellables: Set<AnyCancellable> = []
ItemsPublisher()
.sink { items in
}
.store(&cancellables)
Currently this method is working but it's creating a new database listener (which is an expensive resource) for every ItemsPublisher I create. Instead I want to maintain a single database listener while I have a least 1 subscriber and I want any following subscriber to receive the latest items from the same subscription.
I considered creating a single ItemsPublisher instance and using it throughout the app, but later subscribers didn't receive any data at all.
I also considered using CurrentValueSubject (or a #Published property) to store the items but I couldn't figure out when to setup database listener or when to cancel it for that matter.
Any help or advice would be appreciated.
Instead I want to maintain a single database listener while I have a least 1 subscriber and I want any following subscriber to receive the latest items from the same subscription.
That's exactly what share() is for. View the documentation for more information.
You might also want to consider using multicast with a CurrentValueSubject depending on the situation.

RxSwift onNext not calling scan

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.

RxSwift: Connect to Connectable Observable multipe times

I am adopting the MVVM pattern in my iOS application. I expose a range of Observables as public properties in my view model and bind the UI to these properties. These Observables are created from a private connectable observable.
A view controller class then calls the "execute" method to fire the network request. However, if it fails for any reason, I'd like to be able to call "execute" again but this does not work. I believe this is due to the fact that the connectable observable has completed.
How can I achieve this without having to recreate the view model each time? I know I could do this by transforming a simple execute publish subject to the userDetailsObservable by using flatMap but I rely on the onCompleted event for other functionality. The onCompleted event would be lost as the publish subject remains active.
Connectable Observable Solution
class ViewModel {
public var userName: Observable<String> {
self.userDetailsObservable.map {
return $0["username"]
}
}
public var address: Observable<String> {
self.userDetailsObservable.map {
return $0["address"]
}
}
public func execute() {
self.userDetailsObservable.connect()
}
private lazy var userDetailsObservable: ConnectableObservable<JSON> {
return Observable.create { observer in
// execute network request
// assume there is a json object and error object returned
if error != nil {
observer.onError(error)
} else {
observer.onNext(json)
}
observer.onCompleted()
}.publish()
}
}
The FlatMap solution
This would execute the network request every time an event is pushed on the execute subject. (execute.onNext()). The problem here is that the onCompleted event is lost as we are transforming a publish subject.
class ViewModel {
public var userName: Observable<String> {
self.userDetailsObservable.map {
return $0["username"]
}
}
public var address: Observable<String> {
self.userDetailsObservable.map {
return $0["address"]
}
}
public var execute: PublishSubject<Void>()
private lazy var userDetailsObservable: Observable<JSON> {
return self.execute.flatMapLatest { _ in
Observable.create { observer in
// execute network request
// assume there is a json object and error object returned
if error != nil {
observer.onError(error)
} else {
observer.onNext(json)
}
observer.onCompleted()
}
}.share()
}
You should use catchError and return a default value ("" for instance).
It’s required to prevent the observable from being disposed when you receive an error from the API.

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.

calling cancelAllOperations from nested NSOperations

I'm struggling with the following situation, pleaes bear with me as I've tried to be explain this as clearly as possible:
I have a class CoummintyOperation which is a subclass of a GroupOperation. CommuityOperation is called and added to an NSOperationQueue.
CommunityOperation is in turn calling a whole bunch of NSOperations which are also sublcasses of GroupOperation and in turn are calling NSoperations within them.
I've added dependencies for the GroupOperations in the CommunityOperation class.
The issue I'm dealing with is if a nested operation fails I need to cancel ALL NSOperations in the CommunityOperations class, but I don't have access to the operationQueue that CommunityOperation was added to in order to call the cancelAllOperations method.
Can anyone help me out and explain how I can call this method, cancel All operations in this class (and therefore all nested Operations) and display an error message to the user.
Thanks in advance
Here's some sample code to help explain my issue:
CommunityOperation gets put on an operationQueue from another class (not included)
public class CommunityOperation: GroupOperation {
var updateOperation: UpdateOperation!
var menuOperation: MenuOperation!
var coreDataSaveOperation: CoreDataSaveOperation!
var hasErrors = false
public init() {
super.init(operations: [])
updateOperation = UpdateOperation()
menuOperation = MenuOperation()
coreDataSaveOperation = CoreDataSaveOperation()
coreDataSaveOperation.addDependencies([updateOperation, menuOperation])
self.addOperation(updateOperation)
self.addOperation(menuOperation)
self.addOperation(coreDataSaveOperation)
}
}
MenuOperation class, which has nested Operations in it as well:
class UpdateMenuOperation: GroupOperation {
let downloadGroupsOperation: DownloadGroupsOperation
let downloadMembersOperation: DownloadMembersOperation
init() {
downloadGroupsOperation = DownloadGroupsOperation()
downloadMembersOperation = DownloadMembersOperation(])
super.init(operations: [downloadGroupsOperation,
downloadMembersOperation
])
}
}
DownloadGroupOperation class is again a subclass of GroupOperation. It has 2 operations - the first to download data and the second to parse the data:
class DownloadTopGroupsOperation: GroupOperation {
let downloadOperation: DownloadOperation
let importTopGroupsOperation: ImportOperation
init() {
downloadOperation = DownloadOperation()
importOperation = ImportOperation()
importOperation.addDependency(downloadOperation)
importOperation.addCondition(NoCancelledDependencies())
super.init(operations: [downloadOperation, importOperation])
}
}
And finally (whew) the DownloadOperation class uses a NSURLSession and the method downloadTaskWithURL, it's in this method's completion handler that I want to call cancelAllOperations on the main operatioQueue if an error is returned:
class DownloadOperation: GroupOperation {
init() {
super.init(operations: [])
if self.cancelled {
return
}
let task = session.downloadTaskWithURL(url) { [weak self] url, response, error in
self?.downloadFinished(url, response: response as? NSHTTPURLResponse, error: error)
}
}
func downloadFinished(url: NSURL?, response: NSHTTPURLResponse?, error: NSError?) {
if error {
*cancel allOperations on queue*
}
}
}
It should work in a bit different way. I'd check the isCancelled from the GroupOperation by the end of each NSOperation execution. If operation was cancelled then cancel the current GroupOperation and so on. In the end your CommunityOperation should be cancelled as well.
Here is the rough implementation of the proposed solution:
extension GroupOperation {
func addCancellationObservers() {
self.operations.forEach() { $0.willCancelObservers.append() { [unowned self] operation, errors in
self.cancel() // cancel the group operation. will force it to cancel all child operations
}
}
}
}
Then call addCancellationObservers from the init method of each group operations you have.
if you're using Apple's sample code (or https://github.com/danthorpe/Operations which is the evolution of that project) you can sort this out by attaching a condition to your operations which have dependencies.
Here is the init of your top level GroupOperation
init() {
updateOperation = UpdateOperation()
menuOperation = MenuOperation()
coreDataSaveOperation = CoreDataSaveOperation()
coreDataSaveOperation.addDependencies([updateOperation, menuOperation])
// Attach a condition to ensure that all dependencies succeeded
coreDataSaveOperation.addCondition(NoFailedDependenciesCondition())
super.init(operations: [updateOperation, menuOperation, coreDataSaveOperation])
}
To explain what is happening here... NSOperation has no concept of "failure". The operations always "finish" but whether they finished successfully or failed does not affect how NSOperation dependencies work.
In other words, an operation will become ready when all of its dependencies finish, regardless of whether those dependencies succeeded. This is because "success" and "failure" is something that the subclass must define. Operation (the NSOperation subclass) defines success by having finished without any errors.
To deal with this, add a condition that no dependencies must have failed. In Operations this condition got renamed to make it clearer. But, the concept exists in the Apple sample code too.

Resources