EVReflection
General information
At this moment the master branch is tested with Swift 4.2 and 5.0 beta If you want to continue using EVReflection in an older version, then use the corresponding branch. Run the unit tests to see EVReflection in action.
EVReflection is used in EVCloudKitDao and EVWordPressAPI
In most cases EVReflection is very easy to use. Just take a look the section It's easy to use. But if you do want to do non standard specific things, then EVReflection will offer you an extensive range of functionality.
Available extensions
There are extension available for using EVReflection with XMLDictionairy, Realm, CloudKit, Alamofire and Moya with RxSwift or ReactiveSwift
- XML
- CloudKit
- CoreData
- Realm
- Alamofire
- AlamofireXML
- Moya
- MoyaXML
- MoyaRxSwift
- MoyaRxSwiftXML
- MoyaReactiveSwift
- MoyaReactiveSwiftXML
All these extens can be installed by adding something like this in your podfile:
pod 'EVReflection/MoyaRxSwift'
For Carthage there is not (yet) an extension for all above extension. If needed, please let me know. For carthage you can use:
github "evermeer/EVReflection"
Index
- Main features of EVReflection
- It's easy to use
- If you have XML instead of JSON
- Using EVReflection in your own App
- More Sample code
- Extending existing objects
- Conversion options
- Automatic keyword mapping for Swift keywords
- Automatic keyword mapping PascalCase or camelCase to snake_case
- Custom keyword mapping
- Custom property converters
- Custom object converter
- Custom type converters
- Encoding and Decoding
- Skip the serialization or deserialization of specific values
- Property validators
- Print options
- Deserialization class level validations
- What to do when you use object inheritance
- Known issues
- License
- My other libraries
Main features of EVReflection:
- Parsing objects based on NSObject to and from a dictionary. (also see the XML and .plist samples!)
- Parsing objects to and from a JSON string.
- Support NSCoding function encodeWithCoder and decodeObjectWithCoder
- Supporting Printable, Hashable and Equatable while using all properties.
- Mapping objects from one type to an other
- Support for property mapping, converters, validators and key cleanup
It's easy to use:
Defining an object. You only have to set EVObject as it's base class (or extend an NSObject with EVReflectable):
class User: EVObject {
var id: Int = 0
var name: String = ""
var friends: [User]? = []
}
Parsing JSON to an object:
let json:String = "{\"id\": 24, \"name\": \"Bob Jefferson\", \"friends\": [{\"id\": 29, \"name\": \"Jen Jackson\"}]}"
let user = User(json: json)
Parsing JSON to an array of objects:
let json:String = "[{\"id\": 27, \"name\": \"Bob Jefferson\"}, {\"id\": 29, \"name\": \"Jen Jackson\"}]"
let array = [User](json: json)
Parsing from and to a dictionary:
let dict = user.toDictionary()
let newUser = User(dictionary: dict)
XCTAssert(user == newUser, "Pass")
Saving and loading an object to and from a file:
user.saveToTemp("temp.dat")
let result = User(fileNameInTemp: "temp.dat")
XCTAssert(theObject == result, "Pass")
Mapping object to another type:
let administrator: Administrator = user.mapObjectTo()
If you have XML instead of JSON
If you want to do the same but you have XML, then you can achieve that using the XML subspec 'pod EVReflection/XML' It is a simple way to parse XML. With that your code will look like this:
let xml = "<user><id>27</id><name>Bob</name><friends><user><id>20</id><name>Jen</name></user></friends></user>"
let user = User(xmlString: xml)
Using EVReflection in your own App
'EVReflection' is available through the dependency manager CocoaPods. You do have to use cocoapods version 0.36 or later
You can just add EVReflection to your workspace by adding the following 2 lines to your Podfile:
use_frameworks!
pod "EVReflection"
You can also use the Swift2.2 or Swift2.3 version of EVReflection. You can get that version by using the podfile command:
use_frameworks!
pod "EVReflection"', :git => 'https://github.com/evermeer/EVReflection.git', :branch => 'Swift2.2'
Version 0.36 of cocoapods will make a dynamic framework of all the pods that you use. Because of that it's only supported in iOS 8.0 or later. When using a framework, you also have to add an import at the top of your swift file like this:
import EVReflection
If you want support for older versions than iOS 8.0, then you can also just copy the files from the pod folder to your project. You do have to use the Swift2.3 version or older. iOS 7 support is dropped from Swift 3.
Be aware that when you have your object definitions in a framework and not in your main app, then you have to let EVReflection know that it should also look in that framework for your classes. This can easilly be done by using the following one liner (for instance in the appdelegate)
EVReflection.setBundleIdentifier(YourDataObject.self)
More Sample code
Clone EVReflection to your desktop to see these and more unit tests
func testEquatable() {
var theObjectA = TestObject2()
theObjectA.objectValue = "value1"
var theObjectB = TestObject2()
theObjectB.objectValue = "value1"
XCTAssert(theObjectA == theObjectB, "Pass")
theObjectB.objectValue = "value2"
XCTAssert(theObjectA != theObjectB, "Pass")
}
func testHashable() {
var theObject = TestObject2()
theObject.objectValue = "value1"
var hash1 = theObject.hash
NSLog("hash = \(hash)")
}
func testPrintable() {
var theObject = TestObject2()
theObject.objectValue = "value1"
NSLog("theObject = \(theObject)")
}
func testArrayFunctions() {
let dictionaryArray: [NSDictionary] = yourGetDictionaryArrayFunction()
let userArray = [User](dictionaryArray: dictionaryArray)
let newDictionaryArray = userArray.toDictionaryArray()
}
func testMapping() {
let player = GamePlayer()
player.name = "It's Me"
let administrator = GameAdministrator(usingValuesFrom: player)
}
Direct conversion from a NSDictionary (or an array of NSDictionaries) to json and back.
let dict1: NSDictionary = [
"requestId": "request",
"postcode": "1111AA",
"houseNumber": "1"
]
let json = dict1.toJsonString()
let dict2 = NSMutableDictionary(json: json)
print("dict:\n\(dict1)\n\njson:\n\(json)\n\ndict2:\n\(dict2)")
// You can do the same with arrays
let array:[NSDictionary] = [dict1, dict2]
let jsonArray = array.toJsonStringArray()
let array2 = [NSDictionary](jsonArray: jsonArray)
print("json array: \n\(jsonArray)\n\narray2:\n\(array2)")
This is how you can parse a .plist into an object model. See EVReflectionIssue124.swift to see it working.
if let path = Bundle(for: type(of: self)).path(forResource: "EVReflectionIssue124", ofType: "plist") {
if let data = NSDictionary(contentsOfFile: path) {
let plistObject = Wrapper(dictionary: data)
print(plistObject)
}
}
If you want to parse XML, then you can use the pod subxpec EVReflection/XML
let xml: String = "<data><item name=\"attrib\">itemData</item></data>"
let xmlObject = MyObject(xml: xml)
print(xmlObject)
Extending existing objects:
It is possible to extend other objects with the EVReflectable protocol instead of changing the base class to EVObject. This will let you add the power of EVReflection to objects that also need another framework. In some cases you still need some aditional code. For a sample see the Realm and NSManagedObject subspecs. The most basic way to extend your objects is like this:
import EVReflection
extension MyObject : EVReflectable { }
Extra information:
Conversion options
With almost any EVReflection function you can specify what kind of conversion options should be used. This is done using an option set. You can use the following conversion options:
- None - Do not use any conversion function.
- PropertyConverter : If specified the function propertyConverters on the EVObject will be called
- PropertyMapping : If specified the function propertyMapping on the EVObject will be called
- SkipPropertyValue : If specified the function skipPropertyValue on the EVObject will be called
- KeyCleanup : If specified the automatic pascalCase and snake_case property key mapping will be called.
- Encoding : For if you want class level functionality for encoding values (like base64, unicode, encription, ...)
- Decoding : For if you want class level functionality for decoding values (like base64, unicode, encription, ...)
In EVReflection all functions will use a default conversion option specific to it's function. The following 4 default conversion types are used:
- DefaultNSCoding = [None]
- DefaultComparing = [PropertyConverter, PropertyMapping, SkipPropertyValue]
- DefaultDeserialize = [PropertyConverter, PropertyMapping, SkipPropertyValue, KeyCleanup, Decoding]
- DefaultSerialize = [PropertyConverter, PropertyMapping, SkipPropertyValue, Encoding]
If you want to change one of the default conversion types, then you can do that using something like:
ConversionOptions.DefaultNSCoding = [.PropertyMapping]
Automatic keyword mapping for Swift keywords
If you have JSON fields that are Swift keywords, then prefix the property with an underscore. So the JSON value for self will be stored in the property \_self
. At this moment the following keywords are handled:
"self", "description", "class", "deinit", "enum", "extension", "func", "import", "init", "let", "protocol", "static", "struct", "subscript", "typealias", "var", "break", "case", "continue", "default", "do", "else", "fallthrough", "if", "in", "for", "return", "switch", "where", "while", "as", "dynamicType", "is", "new", "super", "Self", "Type", "COLUMN", "FILE", "FUNCTION", "LINE", "associativity", "didSet", "get", "infix", "inout", "left", "mutating", "none", "nonmutating", "operator", "override", "postfix", "precedence", "prefix", "right", "set", "unowned", "unowned", "safe", "unowned", "unsafe", "weak", "willSet", "private", "public"
Automatic keyword mapping PascalCase or camelCase to snake_case
When creating objects from JSON EVReflection will automatically detect if snake_case (keys are all lowercase and words are separated by an underscore) should be converted to PascalCase or camelCase property names. See Conversion options for when this function will be called.
When exporting object to a dictionary or JSON string you will have an option to specify that you want a conversion to snake_case or not. The default is .DefaultDeserialize which will also convert to snake case.
let jsonString = myObject.toJsonString([.DefaultSerialize])
let dict = myObject.toDictionary([PropertyConverter, PropertyMapping, SkipPropertyValue])
Custom keyword mapping
It's also possible to create a custom property mapping. You can define if an import should be ignored, if an export should be ignored or you can map a property name to another key name (for the dictionary and json). For this you only need to implement the propertyMapping function in the object. See Conversion options for when this function will be called.
public class TestObject5: EVObject {
var Name: String = "" // Using the default mapping
var propertyInObject: String = "" // will be written to or read from keyInJson
var ignoredProperty: String = "" // Will not be written or read to/from json
override public func propertyMapping() -> [(keyInObject: String?, keyInResource: String?)] {
return [(keyInObject: "ignoredProperty",keyInResource: nil), (keyInObject: "propertyInObject",keyInResource: "keyInJson")]
}
}
Custom property converters
You can also use your own property converters. For this you need to implement the propertyConverters function in your object. For each property you can create a custom getter and setter that will then be used by EVReflection. In the sample below the JSON texts 'Sure' and 'Nah' will be converted to true or false for the property isGreat. See Conversion options for when this function will be called.
public class TestObject6: EVObject {
var isGreat: Bool = false
override func propertyConverters() -> [(key: String, decodeConverter: ((Any?) -> ()), encodeConverter: (() -> Any?))] {
return [
( // We want a custom converter for the field isGreat
key: "isGreat"
// isGreat will be true if the json says 'Sure'
, decodeConverter: { self.isGreat = ($0 as? String == "Sure") }
// The json will say 'Sure if isGreat is true, otherwise it will say 'Nah'
, encodeConverter: { return self.isGreat ? "Sure": "Nah"})
]
}
}
Encoding and decoding
You can add generic cod to encode or decode multiple or all properties in an object. This can be used for instance for base64, unicode and encription. Here is a base64 sample:
class SimleEncodingDecodingObject : EVObject{
var firstName: String?
var lastName: String?
var street: String?
var city: String?
override func decodePropertyValue(value: Any, key: String) -> Any? {
return (value as? String)?.base64Decoded?.string ?? value
}
override func encodePropertyValue(value: Any, key: String) -> Any {
return (value as? String)?.base64Encoded.string ?? value
}
}
extension String {
var data: Data { return Data(utf8) }
var base64Encoded: Data { return data.base64EncodedData() }
var base64Decoded: Data? { return Data(base64Encoded: self) }
}
extension Data {
var string: String? { return String(data: self, encoding: .utf8) }
}
Custom object converter
If you want to serialize an object to a dictionary or json but the structure should be different than the object itself, then instead of using propertyConverers, you can also convert the entire object by implementing the customConverter function. In the example below the entire object will be serialized to just a string. You could also return a dictionary that represents the custom structure or an array if the object should have been an array
override func customConverter() -> AnyObject? {
return "Object not serialized"
}
Custom type converter
If you have a custom type that requires special conversion, then you can extend it with the EVCustomReflectable protocol. A good implementation for this can be found in the Realm subspec for the List type. The converter is implemented like this:
extension List : EVCustomReflectable {
public func constructWith(value: Any?) -> EVCustomReflectable {
if let array = value as? [NSDictionary] {
self.removeAll()
for dict in array {
if let element: T = EVReflection.fromDictionary(dict, anyobjectTypeString: _rlmArray.objectClassName) as? T {
self.append(element)
}
}
}
return self
}
public func toCodableValue() -> Any {
return self.enumerated().map { ($0.element as? EVReflectable)?.toDictionary() ?? NSDictionary() }
}
}
For the usage, please have a look at the Realm unittest
Skip the serialization or deserialization of specific values
When there is a need to not (de)serialize specific values like nil NSNull or empty strings you can implement the skipPropertyValue function and return true if the value needs to be skipped. See Conversion options for when this function will be called.
class TestObjectSkipValues: EVObject {
var value1: String?
var value2: [String]?
var value3: NSNumber?
override func skipPropertyValue(value: Any, key: String) -> Bool {
if let value = value as? String where value.characters.count == 0 || value == "null" {
print("Ignoring empty string for key \(key)")
return true
} else if let value = value as? NSArray where value.count == 0 {
print("Ignoring empty NSArray for key\(key)")
return true
} else if value is NSNull {
print("Ignoring NSNull for key \(key)")
return true
}
return false
}
}
Property validators
Before setting a value the value will always be validated using the standard validateValue KVO function. This means that for every property you can also create a validation function for that property. See the sample below where there is a validateName function for the name property.
enum MyValidationError: ErrorType {
case TypeError,
LengthError
}
public class GameUser: EVObject {
var name: String?
var memberSince: NSDate?
var objectIsNotAValue: TestObject?
func validateName(value:AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
if let theValue = value.memory as? String {
if theValue.lengthOfBytesUsingEncoding(NSUTF8StringEncoding) < 3 {
NSLog("Validating name is not long enough \(theValue)")
throw MyValidationError.LengthError
}
NSLog("Validating name OK \(theValue)")
} else {
NSLog("Validating name is not a string: \(value.memory)")
throw MyValidationError.TypeError
}
}
}
Print options
You should be able to solve all problems with parsing your json to an object. If you get warnings and you know they don't matter and you want to stop them from printin you can suppress all print warings by calling the followin line of code:
PrintOptions.Active = .None
If you then want to turn on the print output, then just call:
PrintOptions.Active = .All
It's also possible to enable printing for specific warning types. Here is the line of code that is equal to setting it to .All. Just leave out the type that you want to suppress.
PrintOptions.Active = [.UnknownKeypath, .IncorrectKey, .ShouldExtendNSObject, .IsInvalidJson, .MissingProtocol, .MissingKey, .InvalidType, .InvalidValue, .InvalidClass, .EnumWithoutAssociatedValue]
Deserialization class level validations
There is also support for class level validation when deserializing to an object. There are helper functions for making keys required or not allowed. You can also add custom messages. Here is some sample code about how you can implement such a validation
public class ValidateObject: EVObject {
var requiredKey1: String?
var requiredKey2: String?
var optionalKey1: String?
override public func initValidation(dict: NSDictionary) {
self.initMayNotContainKeys(["error"], dict: dict)
self.initMustContainKeys(["requiredKey1", "requiredKey2"], dict: dict)
if dict.valueForKey("requiredKey1") as? String == dict.valueForKey("optionalKey1") as? String {
// this could also be called in your property specific validators
self.addStatusMessage(.Custom, message: "optionalKey1 should not be the same as requiredKey1")
}
}
}
You could then test this validation with code like:
func testValidation() {
// Test missing required key
let json = "{\"requiredKey1\": \"Value1\"}"
let test = ValidateObject(json: json)
XCTAssertNotEqual(test.evReflectionStatus(), .None, "We should have a not .None status")
XCTAssertEqual(test.evReflectionStatuses.count, 1, "We should have 1 validation result")
for (status, message) in test.evReflectionStatuses {
print("Validation result: Status = \(status), Message = \(message)")
}
}
What to do when you use object inheritance
You can deserialize json to an object that uses inheritance. When the properties are specified as the base class, then the correct specific object type will be returned by the function getSpecificType
. See the sample code below or the unit test in EVReflectionInheritanceTests.swift
class Quz: EVObject {
var fooArray: Array<Foo> = []
var fooBar: Foo?
var fooBaz: Foo?
}
class Foo: EVObject {
var allFoo: String = "all Foo"
// What you need to do to get the correct type for when you deserialize inherited classes
override func getSpecificType(_ dict: NSDictionary) -> EVReflectable {
if dict["justBar"] != nil {
return Bar()
} else if dict["justBaz"] != nil {
return Baz()
}
return self
}
}
class Bar : Foo {
var justBar: String = "For bar only"
}
class Baz: Foo {
var justBaz: String = "For baz only"
}
Known issues
EVReflection is trying to handle all types. With some types there are limitations in Swift. So far there is a workaround for any of these limitations. Here is an overview:
It's not possible in Swift to use .setObjectForKey for:
- nullable type fields like Int?
- properties based on an enum
- an Array of nullable objects like [MyObject?]
- a Set like Set
- generic properties like var myVal:T = T()
- structs like CGRect or CGPoint
For all these issues there are workarounds. The easiest workaround is just using a difrent type like:
- Instead of an Int? you could use NSNumber?
- Instead of [MyObject?] use [MyObject]
- Instead of Set use [MyObject]
- Instead of 'var status: StatysType' use 'var status:Int' and save the rawValue
- Instead of a generic property use a specific property that can hold the data (a dictionary?)
- Instead of using a struct, create your own object model for that struct
If you want to keep on using the same type, You can override the setValue forUndefinedKey in the object itself. See WorkaroundsTests.swift and WorkaroundSwiftGenericsTests.swift to see the workaround for all these types in action.
Generic properties
For generic properties the protocol EVGenericsKVC is required. see WorkaroundSwiftGenericsTests.swift
Arrays with nullable objects or Set's
For arrays with nullable objects or Set's like [MyObj?] or Set the protocol EVArrayConvertable is required. see WorkaroundsTests.swift
Swift Dictionaries
For Swift Dictionaries (and not NSDictionary) the protocol EVDictionaryConvertable is required. See WorkaroundsTests.swift
License
EVReflection is available under the MIT 3 license. See the LICENSE file for more info.
My other libraries:
Also see my other public source iOS libraries:
- EVReflection - Reflection based (Dictionary, CKRecord, JSON and XML) object mapping with extensions for Alamofire and Moya with RxSwift or ReactiveSwift
- EVCloudKitDao - Simplified access to Apple's CloudKit
- EVFaceTracker - Calculate the distance and angle of your device with regards to your face in order to simulate a 3D effect
- EVURLCache - a NSURLCache subclass for handling all web requests that use NSURLReques
- AlamofireOauth2 - A swift implementation of OAuth2 using Alamofire
- EVWordPressAPI - Swift Implementation of the WordPress (Jetpack) API using AlamofireOauth2, AlomofireJsonToObjects and EVReflection (work in progress)
- PassportScanner - Scan the MRZ code of a passport and extract the firstname, lastname, passport number, nationality, date of birth, expiration date and personal numer.
- AttributedTextView - Easiest way to create an attributed UITextView with support for multiple links (url, hashtags, mentions).