I've encountered a similar problem with the way our service endpoint operates as well. It provides a full entity view, along with smaller views of properties on that entity which I'd like to update the model when they are hit.
Example:
class User: Object {
var id: Int
var name: String
var age: Int
var average: Double
likes: List<User>
}
One endpoint gets all 4 fields, another endpoint just gets average
and id
, and a 3rd only likes
and id
, however I'd like to create/update the model from any of them.
Now, I could use create(value:update:)
on the raw JSON thats returned, however my model maps different keynames and does other transforming things (to Dates and other things as well) which I want preserved, so out goes the ability to just call create(value:update:)
with the JSON dict.
We got around this by leveraging Reflection and a protocol called Serializable
, that looks something like this:
protocol Serializable {
func toDictionary() -> [String: Any]
}
extension Serializable {
func toDictionary() -> [String: Any] {
var propertiesDictionary: [String: Any] = [:]
let mirror = Mirror(reflecting: self)
for (propName, propValue) in mirror.children {
guard let propName = propName else { continue }
// Attempt to unwrap the value as AnyObject
if let propValue: AnyObject = self.unwrap(propValue) as AnyObject? {
switch propValue {
case let serializablePropValue as Serializable:
propertiesDictionary[propName] = serializablePropValue.toDictionary()
case let arrayPropValue as [Serializable]:
propertiesDictionary[propName] = Array(arrayPropValue.flatMap { $0.toDictionary() })
case let data as Data:
propertiesDictionary[propName] = data.base64EncodedString(options: .lineLength64Characters)
case _ as Bool: fallthrough
case _ as Int: fallthrough
case _ as Double: fallthrough
case _ as Float: fallthrough
default:
propertiesDictionary[propName] = propValue
}
} else {
// Couldn't treat as AnyObject, treat as Any
switch propValue {
case let arrayPropValue as [Serializable]:
propertiesDictionary[propName] = arrayPropValue.flatMap { $0.toDictionary() }
case let primative as Int8: propertiesDictionary[propName] = primative
case let primative as Int16: propertiesDictionary[propName] = primative
case let primative as Int32: propertiesDictionary[propName] = primative
case let primative as Int64: propertiesDictionary[propName] = primative
case let primative as UInt8: propertiesDictionary[propName] = primative
case let primative as UInt16: propertiesDictionary[propName] = primative
case let primative as UInt32: propertiesDictionary[propName] = primative
case let primative as UInt64: propertiesDictionary[propName] = primative
case let primative as Float: propertiesDictionary[propName] = primative
case let primative as Double: propertiesDictionary[propName] = primative
case let primative as Bool: propertiesDictionary[propName] = primative
case let primative as String: propertiesDictionary[propName] = primative
case let primative as Date: propertiesDictionary[propName] = primative
case let primative as Data: propertiesDictionary[propName] = primative
default: break
}
}
}
return propertiesDictionary
}
/// Unwraps 'any' object.
/// See http://stackoverflow.com/questions/27989094/how-to-unwrap-an-optional-value-from-any-type
/// - parameter any: Any, Pretty clear what this is....
/// - returns: The unwrapped object.
private func unwrap(_ any: Any) -> Any? {
let mi = Mirror(reflecting: any)
guard let displayStyle = mi.displayStyle else { return any }
switch displayStyle {
case .optional:
if mi.children.count == 0 {
return nil
}
if let (_, some) = mi.children.first {
return some
} else {
return nil
}
case .enum:
let implicitTypes: [Any.Type] = [ImplicitlyUnwrappedOptional<Int>.self,
ImplicitlyUnwrappedOptional<String>.self,
ImplicitlyUnwrappedOptional<Double>.self,
ImplicitlyUnwrappedOptional<Bool>.self,
ImplicitlyUnwrappedOptional<Float>.self,
ImplicitlyUnwrappedOptional<Date>.self]
if implicitTypes.contains(where: { $0 == mi.subjectType }) {
if mi.children.count == 0 { return nil }
if let (_, some) = mi.children.first {
return some
} else {
return nil
}
}
return any
default: return any
}
}
With this you can serialize your partially filled object, and then pass with create(value: obj.toDictionary(), update: true)
Couple caveats to note: RealmOptional<T>
, List<T>
, and LinkingObjects<T>
are not handled here, you can add cases for explicit primitive types for RealmOptional<T>
, a skip case for LinkingObjectsBase
, and a skip case with custom handling for ListBase
(reflection /does not/ play nicely with Lists :/)