Handling JSON pagination, and adding data to a table view? - swift

I'm getting a JSON response of the format:
{
"current_page":1,
"data":[
{
"id":1,
"title":"Title 1"
},
{
"id":2,
"title":"Title 2"
},
{
"id":3,
"title":"Title 3"
}
]
}
As you can see, data contains a list of objects, in this case, a list of Posts. Here is my Realm/Objectmapper Post class:
import RealmSwift
import ObjectMapper
class Post: Object, Mappable {
let id = RealmOptional<Int>()
#objc dynamic var title: String? = nil
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
}
}
I created a generic class (I'm not sure it's written right) to handle Pagination responses. I want it to be generic because I have other pagination responses that return Users instead of Posts, among other objects.
Here is my current Pagination class:
import ObjectMapper
class Pagination<T: Mappable>: Mappable {
var data: [T]?
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
data <- map["data"]
}
}
However, I'm not sure if I've written this class right.
And here is the class where I call the endpoint that sends back the pagination data (I've removed irrelevant code):
var posts = [Post]()
provider.request(.getPosts(page: 1)) { result in
switch result {
case let .success(response):
do {
let json = try JSONSerialization.jsonObject(with: response.data, options: .allowFragments)
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Not sure what to do here to handle and retrieve the list of Posts
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Eventually, I need to append the posts to the variable
// self.posts.append(pagination.data)
// Reload the table view's data
self.tableView.reloadData()
} catch {
print(error)
}
case let .failure(error):
print(error)
break
}
}
How do I handle the JSON response correctly in order to get the list of Posts and then append them to the var posts = [Post]() variable? Do I need to make any changes to my Pagination class?

Once you have your json, it is easy to parse it using object mapper:
let pagination = Mapper<Pagination<Post>>().map(JSONObject: json)
It could be further generalized, I have used a direct reference as an example. Your Pagination class can also hold the current page index value.
I think you are also missing the implementation of the mapping(map:) function in your Post class, it should be something like this:
func mapping(map: Map) {
title <- map["title"]
}

Related

Using Object Mapping with Kinvey

I have an array of objects I'm trying to get out of one of my collections. I've followed along using their docs and also some Googling and I believe I'm close to the solution, however not close enough. Here's what I have:
class Clothing: Entity {
var categories: [Category]!
var gender: String!
override class func collectionName() -> String {
//return the name of the backend collection corresponding to this entity
return "categories"
}
override func propertyMapping(_ map: Map) {
super.propertyMapping(map)
categories <- map["clothing"]
gender <- map["gender"]
}
}
class Category: NSObject, Mappable{
var title: String?
var image: String?
convenience required init?(map: Map) {
self.init()
}
func mapping(map: Map) {
title <- map["category"]
image <- map["image"]
}
}
I'm able to get the right gender, but the array of categories doesn't seem to get mapped to the Category object. Any thoughts?
your model actually have one issue, as you can see at https://devcenter.kinvey.com/ios/guides/datastore#Model you should use let categories = List<Category>() instead of var categories: [Category]!. Here's the model that and test and worked:
import Kinvey
class Clothing: Entity {
let categories = List<Category>()
var gender: String!
override class func collectionName() -> String {
//return the name of the backend collection corresponding to this entity
return "clothing"
}
override func propertyMapping(_ map: Map) {
super.propertyMapping(map)
categories <- ("categories", map["categories"])
gender <- ("gender", map["gender"])
}
}
class Category: Object, Mappable{
var title: String?
var image: String?
convenience required init?(map: Map) {
self.init()
}
func mapping(map: Map) {
title <- ("category", map["category"])
image <- ("image", map["image"])
}
}
and here's a sample code how to save a new Clothing object
let casualCategory = Category()
casualCategory.title = "Casual"
let shirtCategory = Category()
shirtCategory.title = "Shirt"
let clothing = Clothing()
clothing.gender = "male"
clothing.categories.append(shirtCategory)
clothing.categories.append(casualCategory)
dataStore.save(clothing) { (result: Result<Clothing, Swift.Error>) in
switch result {
case .success(let clothing):
print(clothing)
case .failure(let error):
print(error)
}
}

Swift Realm - Creating child Realm objects and linking them to their parent

I am currently learning Realm and am converting my experimental app/game which uses arrays to Realm;
It loads pre-seeding data via a local JSON file and ObjectMapper; then creates objects in realm; this part seems to work.
// Parse response
let json = try! JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! Array<Dictionary<String, AnyObject>>
let factories = Mapper<Factory>().mapArray(JSONArray: json)!
do {
let realm = try Realm()
try realm.write {
for factory in factories
{
realm.add(factory, update: true)
}
}
} catch let error as NSError {
print(error.localizedDescription as Any)
}
The issue I'm having is that when it maps; I'd like it to create its child objects at the same time and link them to parent.
Each parent (Factory) has about between 4 children (Engine) linked to it.
// Factory is parent object
class Factory: Object, Mappable {
dynamic var name: String = ""
let engines = List<Engine>()
//Impl. of Mappable protocol
required convenience init?(map: Map) {
self.init()
}
// Mappable
func mapping(map: Map) {
name <- map["name"]
}
}
// Engine is a child to Factory
class Engine: Object {
dynamic var production: Int = 0
// create children and add to the parent factory
static func createEngines(parent:Factory) -> [Engines]
{
var engines:[Engine] = [Engine]()
for _ in stride(from:0, to: 3, by: 1) {
//let engine : Engine = Engine.init(parent: element)
//engines.append(engine)
}
return engines
}
}
If I attempt to put this in my mappable
engines = Engine.createEngines(parent: self)
and make a change in my Factory model;
`var engines = List<Engine>()`
I get this error:
Cannot assign value of type '[Engine]?' to type 'List<Engine>'
The problem here is that simply creating an array of engines (children), appending it to an array doesn't seem to work with Realm and I'm not sure how to do this.
Hence, my question is how do I bulk create children, assign it to a given parent and add it to the current realm write/save?
Many thanks.
I changed my code to do this;
Read all the factories from JSON
Loop through the factories, creating engines
Link the parent object up.
I'm not sure if I did it right but it seems to be working.
I just don't like how I'm having to hardwire the parent; as I thought Realm/ObjectMapper could do that for me. But its not a major issue as there is only about 3 or 4 relationships.
let factories = Mapper<Factory>().mapArray(JSONArray: json)!
do {
let realm = try Realm()
try realm.write {
for f in factories
{
realm.add(f, update: true)
}
let factories = realm.objects(Factory.self)
print (factories.count) // for debug purposes
for f in factories {
for _ in stride(from: 0, to: f.qty, by: 1) {
let engine : Engine = Engine.init()
engine.parent = f
f.engines.append(engine)
}
}
}
} catch let error as NSError {
print(error.localizedDescription as Any)
}
This above code seems to do the work for me; although I do wish I didn't have to manually set the parent (engine.parent = f)
Anyhow, I've accepted #BogdanFarca's answer.
There is a very nice solution by Jerrot here on Githib Gist
The mapping should be defined in your main model object like this:
func mapping(map: Map) {
title <- map["title"]
products <- (map["products"], ArrayTransform<ProductModel>())
}
The real magic is happening in the ArrayTransform class:
func transformFromJSON(value: AnyObject?) -> List<T>? {
var result = List<T>()
if let tempArr = value as! Array<AnyObject>? {
for entry in tempArr {
let mapper = Mapper<T>()
let model : T = mapper.map(entry)!
result.append(model)
}
}
return result
}

RealmSwift + ObjectMapper managing String Array (tags)

What I need to represent in RealmSwift is the following JSON scheme:
{
"id": 1234,
"title": "some value",
"tags": [ "red", "blue", "green" ]
}
Its a basic string array that I'm stumbling on. I'm guessing in Realm I need to represent "tags" as
dynamic id: Int = 0
dynamic title: String = ""
let tags = List<MyTagObject>()
making tags its own table in Realm, but how to map it with ObjectMapper? This is how far I got...
func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
tags <- map["tags"]
}
... but the tags line doesn't compile of course because of the List and Realm cannot use a [String] type.
This feels like a somewhat common problem and I'm hoping someone who has faced this can comment or point to a post with a suggestion.
UPDATE 1
The MyTagObject looks like the following:
class MyTagObject: Object {
dynamic var name: String = ""
}
UPDATE 2
I found this post which deals with the realm object but assumes the array has named elements rather than a simple string.
https://gist.github.com/Jerrot/fe233a94c5427a4ec29b
My solution is to use an ObjectMapper TransformType as a custom method to map the JSON to a Realm List<String> type. No need for 2 Realm models.
Going with your example JSON:
{
"id": 1234,
"title": "some value",
"tags": [ "red", "blue", "green" ]
}
First, create an ObjectMapper TransformType object:
import Foundation
import ObjectMapper
import RealmSwift
public struct StringArrayTransform: TransformType {
public init() { }
public typealias Object = List<String>
public typealias JSON = [String]
public func transformFromJSON(_ value: Any?) -> List<String>? {
guard let value = value else {
return nil
}
let objects = value as! [String]
let list = List<String>()
list.append(objectsIn: objects)
return list
}
public func transformToJSON(_ value: Object?) -> JSON? {
return value?.toArray()
}
}
Create your 1 Realm model used to store the JSON data:
import Foundation
import RealmSwift
import ObjectMapper
class MyObjectModel: Object, Mappable {
#objc dynamic id: Int = 0
#objc dynamic title: String = ""
let tags = List<MyTagObject>()
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
tags <- (map["tags"], StringArrayTransform())
}
}
Done!
This line is the magic: tags <- (map["tags"], StringArrayTransform()). This tells ObjectMapper to use our custom StringArrayTransform I showed above which takes the JSON String array and transforms it into a Realm List<String>.
First of all we should assume that our model extends both Object and Mappable.
Let's create a wrapper model to store the primitive (String) type:
class StringObject: Object {
dynamic var value = ""
}
Then we describe corresponding properties and mapping rules for the root model (not the wrapper one):
var tags = List<StringObject>()
var parsedTags: [String] {
var result = [String]()
for tag in tags {
result.append(tag.value)
}
return result
}
override static func ignoredProperties() -> [String] {
return ["parsedTags"]
}
func mapping(map: Map) {
if let unwrappedTags = map.JSON["tags"] as? [String] {
for tag in unwrappedTags {
let tagObject = StringObject()
tagObject.value = tag
tags.append(tagObject)
}
}
}
We need a tags property to store and obtain the data about tags from Realm.
Then a parsedTags property simplifies extraction of tags in the usual array format.
An ignoredProperties definition allows to avoid some failures with Realm while data savings (because of course we can't store non-Realm datatypes in the Realm).
And at last we are manually parsing our tags in the mapping function to store it in the Realm.
It will work if your tags array will contains a Dictionary objects with a key: "name"
{
"id": 1234,
"title": "some value",
"tags": [ ["name" : "red"], ... ]
}
If you cannot modify JSON object, I recommend you to map json to realm programmatically.
for tagName in tags {
let tagObject = MyTagObject()
tagObject.name = tagName
myObject.tags.append(tagObject)
}
Follow this code
import ObjectMapper
import RealmSwift
//Create a Model Class
class RSRestaurants:Object, Mappable {
#objc dynamic var status: String?
var data = List<RSData>()
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
status <- map["status"]
data <- (map["data"], ListTransform<RSData>())
}
}
//Use this for creating Array
class ListTransform<T:RealmSwift.Object> : TransformType where T:Mappable {
typealias Object = List<T>
typealias JSON = [AnyObject]
let mapper = Mapper<T>()
func transformFromJSON(_ value: Any?) -> Object? {
let results = List<T>()
if let objects = mapper.mapArray(JSONObject: value) {
for object in objects {
results.append(object)
}
}
return results
}
func transformToJSON(_ value: Object?) -> JSON? {
var results = [AnyObject]()
if let value = value {
for obj in value {
let json = mapper.toJSON(obj)
results.append(json as AnyObject)
}
}
return results
}
}

Manually mapping an object using AlamoFireObjectMapper

I'm trying to write some unit tests and need a way to make a dummy version of an object that is mappable. For example:
class MyClassJsonResponse: Mappable {
var status: String?
var response: String?
var errorCode: SAErrorCode?
init() {
}
required init?(_ map: Map) {
}
func mapping(map: Map) {
status <- map["status"]
response <- map["response"]
errorCode <- (map["error_code"], SAErrorCodeTransform())
}
}
Usually this is returned from an Alamofire call, but how would I manually create one and manually pass in an empty JSON string? Any advice on this would be greatly appreciated! Thanks!
The object mapper defines an init function for your classes that allows you to pass a JSON dictionary object. In your test init a JSON object from a string and use that:
let json = JSON.parse("{}")
if let _json = json.dictionaryObject {
if let someObject = SomeObject(JSON: _json) {
// Some assertions here
}
else {
// Some assertions here about failure to map object, etc.
}
}
In my case I'm using this in a QuickSpec and importing SwiftyJSON, but should work in regular XCTest cases.
You can simply use the functions written in Mappable.swift that your class implements
public init?(JSON: [String: Any], context: MapContext? = nil)
public init?(JSONString: String, context: MapContext? = nil)
Inside these functions you will find this code:
if let obj: Self = Mapper(context: context).map(JSON: JSON) {...}
So theoretically you can pass whatever data you want through these functions to test the mapping.

Using Alamofire and Objectmapper the integer value always zero

I am using Alamofire with ObjectMapper and my model class is like that
class Category: Object, Mappable {
dynamic var id: Int = 0
dynamic var name = ""
dynamic var thumbnail = ""
var children = List<Category>()
override static func primaryKey() -> String? {
return "id"
}
required convenience init?(_ map: Map) {
self.init()
}
func mapping(map: Map) {
id <- map["id"]
name <- map["name"]
thumbnail <- map["thumbnail"]
children <- map["children"]
}
}
and I am using Alamofire like that
Alamofire.request(.GET, url).responseArray { (response: Response<[Category], NSError>) in
let categories = response.result.value
if let categories = categories {
for category in categories {
print(category.id)
print(category.name)
}
}
}
the id is always zero, I don't know why?
I fixed it by adding transformation in the mapping function in model class like that
id <- (map["id"], TransformOf<Int, String>(fromJSON: { Int($0!) }, toJSON: { $0.map { String($0) } }))
thanks to #BobWakefield
Does the "id" field exist in the JSON file? If it does not, your initial value of zero will remain. Is the value in quotes in the JSON file? If it is, then it's a string. I don't know if ObjectMapper will convert it to Int.
Moved my comment to an answer.