Mapping JSON response to objects using Rx programming (Moya) - ios

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

Related

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.

common functions issues while converting code from objective c to swift

Currently I have been working on a task of converting code from objective c to swift. The work was going smooth until I occured with a common resuable code that works in objective c but I haven't getting any idea how should I do that in swift.
The scenario working in objective c is.
I have a common function in my dataManager class
- (void)saveRequest:(id)request forId:(NSNumber *)requestId {
WebRequest *requestData = [[WebRequest alloc] initWithEntity:[NSEntityDescription entityForName:WEB_REQUEST inManagedObjectContext:self.context] insertIntoManagedObjectContext:self.context];
requestData.data = [request toJSON];
requestData.requestId = requestId;
requestData.timestamp = [NSDate date];
[self save];
}
in my project the request classes are already created which contains the toJSON function.
from my controller according to user changes I created the request object and passes the request object to this function and this function calls the toJSON function in the request class and everything works in objective c.
But when I convert this function in swift then it didn't support id as function input variable and if I use Any in place of id then it gives an error that Any don't have any toJSON function.
As this function is common different request objects will come from different controllers.
I don't have any idea how should I go further from hear, If anyone have any idea please help me out
Your class should be like
class WebRequest:NSObject
{
var data :Data?
var requestId: NSNumber?
var timestamp: Date?
init(entity:String , insertIntoManagedObjectContext:NSManagedObjectContext)
{
//your code here
}
}
and your code will be as follows
func saveRequest(request:Request, requestId:NSNumber)
{
let requestData = WebRequest(entity: "entityName", insertIntoManagedObjectContext:self.context)
requestData.data = request.toJSON();
requestData.requestId = requestId;
requestData.timestamp = Date()
}
and Request class in which toJson() present
class Request: NSObject
{
//has some members
func toJSON()->Data
{
return Data()
}
}
There is an existing Swift protocol, Codable (or you can do just Encodable if you want, as Codable is merely Encodable and Decodable), which is designed explicitly for representing an object in JSON (or other formats).
You then use JSONEncoder (rather than JSONSerialization, for example) to encode the object into JSON. See Encoding and Decoding Custom Types:
Consider a Landmark structure that stores the name and founding year of a landmark:
struct Landmark {
var name: String
var foundingYear: Int
}
Adding Codable to the inheritance list for Landmark triggers an automatic conformance that satisfies all of the protocol requirements from Encodable and Decodable:
struct Landmark: Codable {
var name: String
var foundingYear: Int
}
You can then do:
let landmark = Landmark(name: "Big Ben", foundingYear: 1859)
do {
let data = try JSONEncoder().encode(landmark)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
That will product JSON like so:
{
"name": "Big Ben",
"foundingYear": 1859
}
See that Encoding and Decoding Custom Types for more information.
But, if you make your types Codable/Encodable, you could then retire your toJSON method entirely. There’s no need to write code to encode JSON anymore.
If you’re looking for a more tactical edit to your project as you convert it from Objective-C to Swift, you could define your own protocol, say JsonRepresentable, that has a single method requirement, your toJSON (or to whatever you’ve renamed this method during your conversion process).
protocol JsonRepresentable {
func toJSON() -> Data
}
And then, for all of the types that have implemented this method, just add this conformance.
Ideally, go back to those individual files and move the method into an extension for that protocol, e.g., for your first object type:
extension RequestObject1: JsonRepresentable {
func toJSON() -> Data {
...
}
}
And for your second:
extension RequestObject2: JsonRepresentable {
func toJSON() -> Data {
...
}
}
Etc.
is not there a simpler way rather than changing it in whole project
I would suggest that the above is best, but, if you don’t want to go back to all of those individual type declarations, you can just add conformance with an empty extension right where you defined JsonRepresentable:
extension RequestObject1: JsonRepresentable { }
extension RequestObject2: JsonRepresentable { }
As long as those types have implemented that method, these extensions will let the compiler know about their conformance to your protocol.
Anyway, this method can then use this protocol:
func save(_ request: JsonRepresentable, requestId: Int) {
let requestData = ...
requestData.data = request.toJSON()
requestData.requestId = requestId
requestData.timestamp = Date()
save()
}

How to have columns in table that don't have to be specified in post request

I have a Vapor app where I want some values to be specified by the user in a POST request, and other values to be computed based on the user-specified values.
For example, suppose the user patches in some new values, and each time that happens the table should automatically update a column with the current time.
I was looking at trying to store the computed properties in the database, but when I modified the model to know about the computed properties, all my POST requests began expecting those to be specified.
What's the most idiomatic way to have columns in a table that don't have to be specified by the post requests?
If you are only looking to update a modified or created timestamp then there are two other ways. In your model, put:
static let createdAtKey: TimestampKey? = \.createdAt
static let updatedAtKey: TimestampKey? = \.updatedAt
var createdAt:Date?
var updatedAt:Date?
And let vapor do it for you, see here. Alternatively, you can make use of the methods willCreate, willUpdate, etc. as described in the docs here if you are updating fields that do not need the user's input.
extension User
{
func willUpdate(on connection: Database.Connection) throws -> Future<User>
{
modifiedCount += 1
return Future.map(on: connection) { self }
}
}
Finally, if you need a bit more flexibility than your own solution, consider using this in your controller:
struct EditUserForm:Content
{
let id:Int
let surname:String
let initials:String
}
func save(_ request:Request) throws -> Future<View>
{
return try request.content.decode(EditUserForm.self).flatMap
{
newUserData in
return try request.parameters.next(User.self).flatMap
{
originalUser in
// update fields as required, EditUserForm only has a subset
return originalUser.save(on:request).transform(to:try self.index(request))
}
}
}
You will need the usual route:
router.post(User.parameter, "save", use:userController.save)
I found that I need to make the computed fields be optional in the model, and then compute them in the route function before saving.
For example:
Making modified_date be optional in the model:
final class MyContentType: PostgreSQLModel {
var id: Int?
var name: String
var modified_date: Date?
}
Setting modified_date to the computed value:
func create(_ request: Request, content: MyContentType) throws -> Future< MyContentType > {
content.modified_date = Date()
return content.save(on: request)
}

Using Realm in iOS swift 4

I am using Realm and it looks good.But I am confused here. Because I did not get how Realm is working and what is its structure of saving data.
I am basically an Android developer and you can say I am newbie in iOS. So in my mind I was thinking That there will be a main file of database. and then inside it, there will be different tables and in tables I can save data.
But I am very amazed that I saved data but It did not asked me for table name and I really do not know how to create table in it. AS in Java we have first create table then we reflect our model as row in that table in database. I am using following file to save Data in Realm . Please have a look and clear following confusions
tell me how realm works?
how the Table is created in realm ?
How to check if db is existed I mean if data is saved already?
How to check if any table let say (Country table) is already created so to retrieve data ??
and see the following class. It is basically a helper class.
public class DbHelper {
private init() {}
static let sharedDbHelper = DbHelper()
var realmObj = try! Realm()
/**
Generic function to create Object in the DB
*/
func save <T: Object> (_ obj : T){
do {
try realmObj.write {
realmObj.add(obj)
}
}catch{
print("DbHelperException","Create",error)
}
}
/**
Generic function to update Object in the DB
*/
func update <T: Object> (_ obj : T, with dictionary: [String : Any?]){
do{
try realmObj.write {
for (key,value) in dictionary{
obj.setValue(value, forKey: key)
}
}
}catch {
print("DbHelperException","Update",error)
}
}
/**
Generic function to delete Object in the DB
*/
func delete <T: Object> (_ obj : T){
do {
try realmObj.write {
realmObj.delete(obj)
}
}catch {
print("DbHelperException","Delete",error)
}
}
/**
Function to manage the error and post it
*/
func postDbError(_ error : Error) {
NotificationCenter.default.post(name: NSNotification.Name(""), object: error)
}
/**
Function to observe the error and post it
*/
func observeDbErrors(in Vc: UIViewController, completion: #escaping (Error?) -> Void) {
NotificationCenter.default.addObserver(forName: NSNotification.Name(""), object: nil, queue: nil) { (notification) in
completion(notification.object as? Error)
}
}
/**
Function to remove observer of the error
*/
func stopDbErrorObserver (in Vc: UIViewController ){
NotificationCenter.default.removeObserver(Vc, name: Notification.Name(""), object: nil)
}
}
I think all your questions are concerned with the same issue, you think that a Realm Database works using tables, however, this is not the case, instead a Realm Database uses Realms. They can be local (your case), in-memory, or synchronized.
A Realm can contain multiple kinds of objects, which would represent the tables you are thinking of.
If you want to have different tables, the solution would be to define multiple RealmObjects.
To check if your Realm doesn't contain any instance of a specific object you could do something like this: database.objects(YourRealmObject).isEmpty()
The equivalent to a "table" in Realm is a model that you define as a class . You can read about this here: https://realm.io/docs/swift/latest/#models
Here's an example that Realm provides:
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var owner: Person? // Properties can be optional
}
So in this case, Dog is your table name, and name and owner are your column names (each with a specific data type).

Design pattern for Realm persistence

I'm using Realm in a App and I'm trying to abstract much as possible so that in the future I can swap database providers without too much change.
This pattern has worked well although I'm concerned about the following.
Is creating a new realm object everytime a overhead (My current understanding is that Realm Objects are cached internally)?
Are there any problems with the way I'm using Realm ?
Are there any better design patterns for my purpose ?
public struct BookDataLayer: BookDataLayerProvider {
func isBookAvailable(bookIdentifier: String) throws -> Bool {
let database = try getDatabase()
return !database.objects(Book).filter("identifier = %#", bookIdentifier).isEmpty
}
func createOrUpdateBook(bookIdentifier: String, sortIndex: Int) throws {
let book = Book()
Book.bookIdentifier = bookIdentifier
Book.sortIndex = sortIndex
try create(book, update: true)
}}
protocol BookDataLayerProvider : DataAccessLayer {
func isBookAvailable(bookIdentifier: String) throws -> Bool
func createOrUpdateBook(bookIdentifier: String, sortIndex: Int) throws n}
extension DataAccessLayer {
func getDatabase() throws -> Realm {
do {
let realm = try Realm()
// Advance the transaction to the most recent state
realm.refresh()
return realm
} catch {
throw DataAccessError.DatastoreConnectionError
}
}
func create(object: Object, update: Bool = false) throws {
let database = try self.getDatabase()
do {
database.beginWrite()
database.add(object, update: update)
// Commit the write transaction
// to make this data available to other threads
try database.commitWrite()
} catch {
throw DataAccessError.InsertError
}
}}
// Usage
let bookDataLayer = BookDataLayer()
bookDataLayer.isBookAvailable("4557788")
bookDataLayer.createOrUpdateBook("45578899", 10)
That's a completely solid design pattern. It's pretty common for developers to abstract the data layer APIs way from their code in case they need to switch it out.
In response to your questions:
You're correct. Realm object instances are internally cached, so you can easily call let realm = try! Realm() multiple times with very little overhead.
Unless you've found a specific reason, it's probably not necessary to call refresh() on the Realm instance every time you use it. Realm instances on the main thread are automatically refreshed on each iteration of the run loop, so you only need to call refresh() if you're expecting changes on a background thread, or need to access changes before the current run loop has completed.
'Better' design patterns is probably a matter of opinion, but from what I've seen from other codebases, what you've got there is already great! :)

Resources