I am new to RxSwift and whole concept of RX and I would like to know how to handle global application state fetched from remote server by RxSwift.
Let's assume I need to fetch JSON and parse it to list of objects to show it in table view but also I need to create map in format [{id: object}, ...] to use the data in other sections of application.
For example: App repetitively fetches a person list from server and needs the data for person table view as for persons messages to display avatar and status with related message. So the data are needed for view models PersonViewModel and MessageViewModel composed by models Person and Message.
Would be the correct way to have such structure:
struct Person {
let id: personId
let fullName: String
let status: personStatus
}
class PeopleStore {
var order: [personId] = []
var dataMap: [personId: Person] = [:]
init(people: [Person]) {
order = people.map { $0.id }
for person in people {
dataMap[person.id] = person
}
}
}
class AppState {
let rx_peopleStore: Variable<PeopleStore>
init(peopleStore: PeopleStore) {
self.rx_peopleStore = Variable(peopleStore)
}
}
And to adjust the app state by fetch from server:
...
_ = PeopleApi
.rx_peopleStore
.asDriver(onErrorJustReturn: [])
.driveNext { peopleStore in
sharedAppState.rx_peopleStore.value = peopleStore
}
...
And in viewModels:
...
_ = sharedAppState
.rx_peopleStore
.asDriver()
.driveNext { store in
// refreshUI by data from store
}
.addDisposableTo(bag)
...
Is this correct way or exists some different and better approach? I would like to also (in future) the fetched data persist. What is the best practice? Thank you.
P.S. sorry for typos in code, if are there. I just wrote it without compiling.
I had a similar problem with keeping recent state of different things (like server responses, geolocation, etc.) and eventually made a lightweight Rx-based framework for this which I am using ever since, have a look whether it suits your needs as well - https://github.com/maxvol/RaspSwift
Related
I have three different view controllers: (in this order) first+last name, birthday, email+password.
I have already connected my app to firebase and I know how to send the user information to firebase, but only for one of the view controllers. I want firebase to store all of the information from all three view controllers (name, birthday, and email/password) after clicking the "sign up" button on the last view controller (email+password). Please let me know how I can combine all of the information to one new user, rather than making them all new users.
It seems like you are making multiple API calls rather than one single API to sign up a new user, meaning only one call is necessary. There are a couple of different ways you could do this, but the main idea is that you need to get all the data to the very end of the onboarding sign up and then call the Firebase API.
I suggest you make a data object called NewUser and store the data as you progress through the sign-up process. It would look something like this:
class NewUser {
// MARK: - Variables
var userID:String
var name:String?
var birthday:String?
var email:String?
var password:String?
// MARK: - Init Variables
init(userID:String, name:String, birthday:String, email:String, password:String) {
self.userID = userID
self.name = name
self.birthday = birthday
self.email = email
self.password = password
}
}
Example to set name data:
NewUser.name = name
Then call NewUser.name to access the stored data.
I came across a strange behaviour. Even though my unit tests are passing, I don;t quite understand this.
func testDeleteMoment() {
// Create a Moment for current user
let expectCreate = expectation(description: "create moment should succeed")
Datastore.shared.createMoment(notes: "myNotes", rating: 1, time: Date(), location: "", isPublic: true, completion: {(success, error) in
XCTAssertTrue(success)
expectCreate.fulfill()
})
waitForExpectations(timeout: 5) { (error) in
XCTAssertNil(error, "Test timed out. \(String(describing: error?.localizedDescription))")
}
let query = PFQuery(className: "Moment")
var objectId = ""
do {
query.whereKey("owner", equalTo:PFUser.current()!)
let object = try query.getFirstObject()
objectId = object.objectId!
} catch {}
let task = Datastore.shared.deleteMoment(id: objectId)
task.waitUntilFinished()
let query2 = PFQuery(className: "Moment")
query2.whereKey("owner", equalTo:PFUser.current()!)
let task3 = query2.countObjectsInBackground()
task3.waitUntilFinished()
XCTAssertEqual(task3.result, 0)
}
While writing my datastore.deleteMoment(), I noticed that unlike saveEventually(), deleteEventually() doesn't have a completion handler. Instead, it only comes with a BFTask<NSNumber>. Hence I experimented with the following code and the unit test passes to my surprise.
func deleteMoment(id: String) -> BFTask<NSNumber> {
let pfMoment = PFObject(className:"Moment")
pfMoment.objectId = id
return pfMoment.deleteEventually()
}
How comes that I don't have to retrieve the object before hand in order to delete it? Why isn't there then just a method to delete the object via an id, instead of doing it like this?
How comes that I don't have to retrieve the object before hand in order to delete it?
Parse lets you work with "shell objects". Basically, if you know the object id you can attach it to a blank instance of a Parse Object subclass and do some actions on it. You can create a shell, assign a couple values, and call object.save() and it will update just those fields. You can create a blank object, assign an id, then call object.fetch() to obtain it, without having to have gotten the object from a query or pointer on another object.
The reason it works is because in order to perform the database operations, the object id and class name are the only pieces of information required. If you just want to update a couple fields, you don't need to pull all of the rest of the data to do so. And if you want to destroy an object, why would you need all of it's data locally first? You just need to remove the entry in the database with the matching _id.
Why isn't there then just a method to delete the object via an id, instead of doing it like this?
Nobody has built it yet. Parse is open source, and while it's a pretty phenomenal resource today, there's certainly a lot of room for improvement. Feel free to add this feature and create a PR. Though you could easily build your own wrapper by extending the Parse.Object class with this function, that basically does what you already did under the hood.
I want all all the HTTP GET requests to the API generated by Sails to be restricted. So how can I apply a filter to all incoming API GET requests.
More specifically, most of my models have an attribute called publicityLevel. This tells whether a model is public or not. So I want all my models to automatically apply a filter (like publicityLevel: 'public') for all incoming GET requests.
Even more advanced, I'd like to write some code which decides whether the user can see a specific model or not. So if a user is an admin, don't apply this filter. If the user isn't an admin, apply this filter.
I had similar problem to solve with blueprints and I solved it.
If we are talking about BLUEPRINTS:
You can get modelName from req.options.model when you are using Blueprints.
I was using it to check if user belongs to the same group as element.
Unfortunately you can't use this[modelName] as option is giving you model name starting with small letter, so first you have to upper case first letter with e.g. var modelName = req.options.model.charAt(0).toUpperCase() + req.options.model.slice(1);
and then you are free to use this[modelName].whateverYouNeed
I used it for generic policy to let user editing only his own group elements.
var modelName = req.options.model.charAt(0).toUpperCase() + req.options.model.slice(1)
var elementID = null
if (req.params.id) { // To handle DELETE, PUT
elementID = req.params.id
}
if (req.body.id) { // To handle POST
elementID = req.body.id
}
this[modelName].findOne({
id: elementID
}).exec(function(err, contextElement) {
if(err) {
return res.serverError(err)
}
if(contextElement.group=== req.user.group.id) {
sails.log('accessing own: ' + modelName)
return next()
}
else {
return res.forbidden('Tried to access not owned object')
}
})
I want to be able to fetch data from an external Api for a specific request, but when that data is returned, also make it available in the cache, to represent the current state of the application.
This solution seems to work:
var Rx = require('rx');
var cached_todos = new Rx.ReplaySubject(1);
var api = {
refresh_and_get_todos: function() {
var fetch_todos = Rx.Observable.fromCallback($.get('example.com/todos'));
return fetch_todos()
.tap(todos => cached_todos.onNext(todos));
},
current_todos: function() {
return cached_todos;
}
};
But - apparently Subjects are bad practice in Rx, since they don't really follow functional reactive programming.
What is the right way to do this in a functional reactive programming way?
It is recommended not to use Subjects because there is a tendency to abuse them to inject side-effects as you have done. They are perfectly valid to use as ways of pushing values into a stream, however their scope should be tightly constrained to avoid bleeding state into other areas of code.
Here is the first refactoring, notice that you can create the source beforehand and then your api code is just wrapping it up in a neat little bow:
var api = (function() {
var fetch_todos = Rx.Observable.fromCallback($.get('example.com/todos'))
source = new Rx.Subject(),
cached_todos = source
.flatMapLatest(function() {
return fetch_todos();
})
.replay(null, 1)
.refCount();
return {
refresh: function() {
source.onNext(null);
},
current_todos: function() {
return cached_todos;
}
};
})();
The above is alright, it maintains your current interface and side-effects and state have been contained, but we can do better than that. We can create either an extension method or a static method that accepts an Observable. We can then simplify even further to something along the lines of:
//Executes the function and caches the last result every time source emits
Rx.Observable.withCache = function(fn, count) {
return this.flatMapLatest(function() {
return fn();
})
.replay(null, count || 1)
.refCount();
};
//Later we would use it like so:
var todos = Rx.Observable.fromEvent(/*Button click or whatever*/))
.withCache(
Rx.Observable.fromCallback($.get('example.com/todos')),
1 /*Cache size*/);
todos.subscribe(/*Update state*/);
I have a web application with an Angular / Breeze client side calling into a Breeze Web API, which uses an Entity Framework code first model. I have a datacontext (Angular service) responsible for all communications with server.
I would like to completely separate the server development from the client side development so developers need not even have .NET installed on their system. I would like the solution to require very little coding in way of creating fakes, because the app is changing frequently and I do not want to have to rewrite fakes every time my implementation changes. I have a bunch of test data in the database that I would like to make available on the client.
What is a good way (standard way?) to achieve this?
Just create mocks. You don't even have to make a RESTful call if you don't want to, just have your service decide whether to hit the server or pull from cache and load up your cache locally on start -
function loadMocks (manager) {
var personMockOne = manager.createEntity('Person', { id: 1, firstName: 'John', lastName: 'Smith' });
var companyMockOne = manager.createEntity('Company', { id: 1, name: 'Acme Inc.' });
companyMockOne.employees.push(personMockOne);
}
http://pwkad.wordpress.com/2014/02/02/creating-mocks-with-breeze-js/
To Expand...
Doing this requires a bit of extra set up. I personally always write my queries separate from my controller / view model logic through a service which takes parameters. A few example parameters are always something like parameters and forceRemote. The idea is that when you go to execute the query you can decide whether to hit the server or query locally. A quick example -
function queryHereOrThere (manager, parameters, forceRemote) {
var query = breeze.EntityQuery().from('EntityName').using(manager);
query.where(parameters);
if (!forceRemote) {
query.executeQueryLocally();
} else {
query.executeQuery();
}
}
Here is my current solution.
Get data from the server with a 'unit test' that creates a Breeze Web API controller and uses it to gather the breeze metadata and all the test data from the database, then writes that data to testData.json and breezeMetadata.json.
Abstract the creation of the Breeze Entity Manager to an Angular service entityManager.
Create a fakeEntityManager Angular service, which: 1) creates the entity manager, 2) overrides the EntityManager.executeQuery function to always use the local version, and 3) loads up the mgr with the test data. The code for that service is below.
In the datacontext service, use the $injector service to conditionally inject a real or a fake entity manager.
datacontext.js
angular.module('app').factory('datacontext', ['$injector','config', datacontext]);
function datacontext($injector, config) {
if (config.useLocalData === true) {
var mgr = $injector.get('fakeEntityManager');
} else var mgr = $injector.get('entityManager');
...
fakeEntityManager.js
(function() {
'use strict';
var serviceId = 'fakeEntityManager';
angular.module('app').factory(serviceId, ['breeze', 'common', em]);
function em(breeze, common) {
var $q = common.$q;
var mgr = getMgr();
populateManager(["Projects", "People", "Organizations"]);
return mgr;
function getMgr() {
breeze.EntityManager.prototype.executeQuery = function(query) {
return $q.when(this.executeQueryLocally(query)).then(function (results) {
var data = {
results: results
};
if (query.inlineCountEnabled == true) data.inlineCount = results.length;
return data;
});
};
var metaData = < PASTE JSON HERE >
new breeze.ValidationOptions({ validateOnAttach: false }).setAsDefault();
var metadataStore = new breeze.MetadataStore();
metadataStore.importMetadata(metaData, true);
return new breeze.EntityManager({
dataService: new breeze.DataService(
{
serviceName: "fakeApi",
hasServerMetadata: false // don't ask the server for metadata
}),
metadataStore: metadataStore
});
}
function populateManager(resources) {
var testData = < PASTE JSON HERE >;
resources.forEach(function (resource) {
testData[resource].forEach(function (entity) {
mgr.createEntity(mgr.metadataStore.getEntityTypeNameForResourceName(resource), entity);
});
});
}
}
})();
If you don't use inlineCount queries there is no need to override executeQuery. You can just add the following property to the EntityManager constructor's parameter:
queryOptions: new breeze.QueryOptions({ fetchStrategy: breeze.FetchStrategy.FromLocalCache })
Todo: Override the EntityManager.saveChanges() function (or somehow configure the entity manager) to prevent calls to the server while still allowing entities to be edited and saved locally.