RealmSwift + ObjectMapper managing String Array (tags) - swift

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

Related

Manually modifying model property values in vapor 4 response

I have a vapor 4 application. I do a query from database for getting some items and I want to perform some manual calculation based on the returned values before finishing the request. here a sample code of what I am trying to achieve.
final class Todo: Model, Content {
static var schema: String = "todos"
#ID(custom: .id)
var id: Int?
#Field(key: "title")
var title: String
var someValue: Int?
}
/// Allows `Todo` to be used as a dynamic migration.
struct CreateTodo: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Todo.schema)
.field("id", .int, .identifier(auto: true))
.field("title", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Todo.schema).delete()
}
}
final class TodoController:RouteCollection{
func boot(routes: RoutesBuilder) throws {
routes.get("tmp", use: temp)
}
func temp(_ req:Request) throws -> EventLoopFuture<[Todo]> {
Todo.query(on: req.db).all().map { todos in
todos.map {
$0.someValue = (0...10).randomElement()!
return $0
}
}
}
}
The problem is that those manual changes, aren't available in response. In this case someValue property.
Thanks.
[
{
"title": "item 1",
"id": 1
},
{
"title": "item 2",
"id": 2
}
]
The problem you're hitting is that Models override the Codable implementations. This allows you to do things like passing around parents and not adding children etc.
However, this breaks your case. What you should do, is create a new type if you want to return a Todo with another field that isn't stored in the database, something like:
struct TodoResponse: Content {
let id: Int
let title: String
let someValue: Int
}
And then convert from your database type to your response type in your route handler (this is a pretty common pattern and the recommended way to do it in Vapor)

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

How to decode [String: Any] using Argo

I just learned Argo basics and was able to decode 99% of my JSONs in production. Now I am facing the following structure (the keys like "5447" and "5954" are dynamic) and need help:
{
"5447": {
"business_id": 5447,
"rating": 5,
"comment": "abcd",
"replies_count": 0,
"message_id": 2517
},
"5954": {
"business_id": 5954,
"rating": 3,
"comment": "efgh",
"replies_count": 0,
"message_id": 633
}
}
The typical sample of Argo decoding is like:
struct User {
let id: Int
let name: String
}
for JSON structure (keys are fixed "id" and "name"):
{
"id": 124,
"name": "someone"
}
using something like this:
extension User: Decodable {
static func decode(j: JSON) -> Decoded<User> {
return curry(User.init)
<^> j <| "id"
<*> j <| "name"
}
}
However the data structure I need to parse doesn't fit the example.
UPDATE: using Tony's first implementation with a small modification in the last line, I got my job done. Here is the complete working code:
Business.swift:
import Argo
import Curry
import Runes
struct Business {
let businessID: Int
let rating: Double?
let comment: String?
let repliesCount: Int?
let messageID: Int?
}
extension Business: Decodable {
static func decode(_ json: JSON) -> Decoded<Business> {
let c0 = curry(Business.init)
<^> json <| "business_id"
<*> json <|? "rating"
return c0
<*> json <|? "comment"
<*> json <|? "replies_count"
<*> json <|? "message_id"
}
}
Businesses.swift
import Argo
import Runes
struct Businesses {
let businesses: [Business]
}
extension Businesses: Decodable {
static func decode(_ json: JSON) -> Decoded<Businesses> {
let dict = [String: JSON].decode(json)
let arr = dict.map { Array($0.map { $1 }) }
let jsonArr = arr.map { JSON.array($0) }
return Businesses.init <^> jsonArr.map([Business].decode).value ?? .success([])
}
}
When you have keys in a dictionary that are dynamic, you'll have to step out of the convenient operators that Argo provides. You'll first need to get the JSON object element then map over it yourself. Since the keys are irrelevant here (because the ids are also in the embedded dictionaries) it actually won't be too bad. The easiest way is to probably make a new struct to wrap this:
struct Businesses: Decodable {
let businesses: [Business]
static func decode(_ json: JSON) -> Decoded<Businesses> {
// Get dictionary
let dict: Decoded<[String: JSON]> = [String: JSON].decode(json)
// Transform dictionary to array
let array: Decoded<[JSON]> = dict.map { Array($0.map { $1 }) }
// Wrap array back into a JSON type
let jsonArray: Decoded<JSON> = array.map { JSON.array($0) }
// Decode the JSON type like we would with no key
return Businesses.init <^> array.map([Business].decode)
}
}
What we're doing here is getting the dictionary and transforming it to an array so we can decode it like any other array.
You could also skip the transformation to an array part and decode it from the dictionary like so:
static func decode(_ json: JSON) -> Decoded<Businesses> {
// Get dictionary
let dict: Decoded<[String: JSON]> = [String: JSON].decode(json)
// Map over the dictionary and decode the values
let result: Decoded<[Businesses]> = dict.flatMap { object in
let decoded: [Decoded<Business>] = Array(object.map { decode($1) })
return sequence(decoded)
}
return Businesses.init <^> result
}
I didn't try any of this code so there might be some tweaks. Also, you probably don't need all the type annotations but I added them to help explain the code. You might also be able to use this code outside of a new struct model depending on your application.

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.

Access properties via subscripting in Swift

I have a custom class in Swift and I'd like to use subscripting to access its properties, is this possible?
What I want is something like this:
class User {
var name: String
var title: String
subscript(key: String) -> String {
// Something here
return // Return the property that matches the key…
}
init(name: String, title: String) {
self.name = name
self.title = title
}
}
myUser = User(name: "Bob", title: "Superboss")
myUser["name"] // "Bob"
Update: The reason why I'm looking for this is that I'm using GRMustache to render from HTML templates. I'd like to be able to just pass my model object to the GRMustache renderer…
GRMustache fetches values with the keyed subscripting objectForKeyedSubscript: method and the Key-Value Coding valueForKey: method. Any compliant object can provide values to templates.
https://github.com/groue/GRMustache/blob/master/Guides/view_model.md#viewmodel-objects
This is a bit of a hack using reflection. Something along the lines of the following could be used.
protocol PropertyReflectable { }
extension PropertyReflectable {
subscript(key: String) -> Any? {
let m = Mirror(reflecting: self)
for child in m.children {
if child.label == key { return child.value }
}
return nil
}
}
struct Person {
let name: String
let age: Int
}
extension Person : PropertyReflectable {}
Then create a Person and access it's keyed properties.
let p = Person(name: "John Doe", age: 18)
p["name"] // gives "John Doe"
p["age"] // gives 18
You could modify the subscript to always return an interpolated string of the property value.
Adding some syntax sugar to Benzi's answer:
protocol PropertyReflectable { }
extension PropertyReflectable {
subscript(key: String) -> Any? {
let m = Mirror(reflecting: self)
return m.children.first { $0.label == key }?.value
}
}
struct Person: PropertyReflectable {
let name: String
let age: Int
}
Then create a Person and access it's keyed properties.
let p = Person(name: "John Doe", age: 18)
p["name"] // gives "John Doe"
p["age"] // gives 18
Using valueForKey should enable you to access properties using their names. Be sure that you're working with a object that inherit NSObject
class people: NSObject {
var age: NSString = "44"
var height: NSString = "153"
}
let person:people = people()
let stringVariable = "age"
person.valueForKey("age")
// Print "44"
person.valueForKey("\(stringVariable)")
// Print "44"
(GRMustache author here)
Until a swift-oriented Mustache library is out, I suggest having your classes inherit from NSObject (so that they have the valueForKey: method). GRMustache will then fetch values with this method.
In case this would still not work (blank values in the rendering), you may try to disable GRMustache security features (see https://github.com/groue/GRMustache/blob/master/Guides/security.md#disabling-safe-key-access)
Should you experience any other trouble, please open an issue right into the repository: https://github.com/groue/GRMustache/issues
EDIT February 2, 2015: GRMustache.swift is out: http://github.com/groue/GRMustache.swift
Shim's answer above doesn't work anymore in Swift 4. There are two things you should be aware of.
First of all, if you want to use value(forKey:) function, your class must inherit NSObject.
Secondly, since Objective-C doesn't know anything about value type, you have to put the #objc keyword in front of your value type properties and Swift will do the heavy-lifting for you.
Here is the example:
import Foundation
class Person: NSObject {
#objc var name: String = "John Dow"
#objc var age: Int = 25
#objc var height: Int = 180
subscript(key: String) -> Any? {
return self.value(forKey: key)
}
}
let person: Person = Person()
person["name"] // "John Dow"
person["age"] // 25
person["height"] // 180
I suppose you could do:
class User {
let properties = Dictionary<String,String>()
subscript(key: String) -> String? {
return properties[key]
}
init(name: String, title: String) {
properties["name"] = name
properties["title"] = title
}
}
Without knowing your use case I would strongly advise against doing this.
Another approach:
class User {
var name : String
var title : String
subscript(key: String) -> String? {
switch key {
case "name" : return name
case "title" : return title
default : return nil
}
}
init(name: String, title: String) {
self.name = name
self.title = title
}
}
It might be worth noting that Swift doesn't appear to currently support reflection by names. The reflect function returns a Mirror whose subscript is Int based, not String based.