Swift is fun and cool, but Objc is a tank. Here is my favorite hack in objc adapted to swift.
After about two years of iOS development I really got tired of integrating web services into my application. If a login request succedes it would return something similar to this response:
{
"status" : "success",
"user" : {
"first_name" : "Andrei",
"last_name" : "Puni",
"age" : 22
}
}
After handling the networking part we usually get a NSDictionary
with the values from the json/xml response. Now all we need to do is write the User
class and create a User
instance from that information:
class User {
var firstName: String = ""
var lastName: String = ""
var age: Int = 0
class func createFrom(info: NSDictionary) -> User {
var user = User()
if let firstName = info["first_name"] as? String {
user.firstName = firstName
}
if let lastName = info["last_name"] as? String {
user.lastName = lastName
}
if let age = info["age"] as? Int {
user.age = age
}
return user
}
}
This code is pretty simple and does the job, but it has some issues:
- if you add or remove a property from the model you must also mirror the changes in the
createFrom
method - you have to write code every time you create a new model
- it’s repetitive and boring to code
The same problem appears when sending data to the server.
The Objective-c Runtime allows you to look under the hood of your code and play with it while it is bening executed. You can see the properties and methods of a class, it’s superclass, it’s subclasses and the methods and properties from a protocol. You can even change the way your program works at runtime by creating a new class, changing it’s super class or swizzling it’s methods.
By using Key Value Coding and the Objective-C Runtime to get the property list of the current class we can implement a NSObject
extension that will give a general implementation for loading data from a JSON and converting an object to a NSDictionary
:
extension NSObject {
// Creates an object from a dictionary
class func fromJson(jsonInfo: NSDictionary) -> Self {
var object = self()
(object as NSObject).load(jsonInfo)
return object
}
func load(jsonInfo: NSDictionary) {
for (key, value) in jsonInfo {
let keyName = key as String
if (respondsToSelector(NSSelectorFromString(keyName))) {
setValue(value, forKey: keyName)
}
}
}
func asJson() -> NSDictionary {
var json = NSMutableDictionary.dictionary()
for name in propertyNames() {
if let value: AnyObject = valueForKey(name) {
json[name] = value
}
}
return json
}
func propertyNames() -> [String] {
var names: [String] = []
var count: UInt32 = 0
// Uses the Objc Runtime to get the property list
var properties = class_copyPropertyList(classForCoder, &count)
for var i = 0; i < Int(count); ++i {
let property: objc_property_t = properties[i]
let name: String =
NSString.stringWithCString(property_getName(property), encoding: NSUTF8StringEncoding)
names.append(name)
}
free(properties)
return names
}
}
When you make a subclass of NSObject
the methods fromJson()
and asJson()
will just work.
class User: NSObject {
var firstName: String = ""
var lastName: String = ""
var age: Int = 0
}
let info = [
"firstName": "Andrei",
"lastName": "Puni",
"age": 23
]
var user = User.fromJson(info)
println(user.firstName) // "Andrei"
println(user.lastName) // "Puni"
println(user.age) // 23
println(user.asJson())
//{
// age = 23;
// firstName = Andrei;
// lastName = Puni;
//}
This solution can be extended to support snake_case
JSONs and camelCase
objects.
func underscoreToCamelCase(string: String) -> String {
var items: [String] = string.componentsSeparatedByString("_")
var camelCase = ""
var isFirst = true
for item: String in items {
if isFirst == true {
isFirst = false
camelCase += item
} else {
camelCase += item.capitalizedString
}
}
return camelCase
}
extension NSObject {
...
func load(jsonInfo: NSDictionary) {
for (key, value) in jsonInfo {
let keyName = key as String
if (respondsToSelector(NSSelectorFromString(keyName))) {
setValue(value, forKey: keyName)
} else {
let camelCaseName = underscoreToCamelCase(keyName)
if (respondsToSelector(NSSelectorFromString(camelCaseName))) {
setValue(value, forKey: camelCaseName)
}
}
}
}
...
}
let info = [
"first_name": "Andrei",
"last_name": "Puni",
"age": 23
]
var user = User.fromJson(info)
println(user.firstName) // "Andrei"
println(user.lastName) // "Puni"
println(user.age) // 23
println(user.asJson())
//{
// age = 23;
// firstName = Andrei;
// lastName = Puni;
//}
There are a lot of optimizations that can be done on the load
function. The Obj-C version I use is in APUtils, you can read the source code if you want to see what optimizations can be done.
This hack can also be applied to other kinds tasks. For example: loading data from a object in a table view cell.
// a reddit post
class Post: NSObject {
var title: String = ""
var url: String = ""
var ups: Int = 0
var downs: Int = 0
var score: Int = 0
}
// cell for reddit post
class PostCell: UITableViewCell {
@IBOutlet var titleLabel: UILabel!
@IBOutlet var scoreLabel: UILabel!
func loadPost(post: Post) {
self.titleLabel.text = post.title
self.scoreLabel.text = "\(post.score)"
}
}
We can implement a UITableViewCell
extension that has a general implementation for loading data from an object. It does this by using a naming convention: all label properties should end with Label
(ex. titleLabel
).
extension UITableViewCell {
func loadInfo(info: NSDictionary) {
let propertyNames = self.propertyNames()
let labels: [String] = propertyNames.filter { (name: String) -> Bool in
return name.hasSuffix("Label")
}
for label in labels {
let propertyName = (label + "$$$").stringByReplacingOccurrencesOfString("Label$$$", withString: "")
if let value: AnyObject = info[propertyName] {
let l: UILabel? = valueForKey(label) as UILabel?
if l != nil {
let textValue = "\(value)"
l!.text = textValue
}
}
}
}
}
Now our implementation of PostCell
will look like this:
class PostCell: UITableViewCell {
@IBOutlet var titleLabel: UILabel!
@IBOutlet var scoreLabel: UILabel!
}
In interface builder we edit the interface and connect the IBOutlets
.
And before passing the cell to the table view, we call the loadInfo
method.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var posts: [Post] = []
...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as PostCell
var post = self.posts[indexPath.row]
cell.loadInfo(post.asJson())
return cell
}
...
}
You can get the code from github or zip.
If you found this usefull, please take a moment and share it with your friends.
Thanks, nice article. One thing though, as part of hearting Swift, I’ve started trying to adopt a more function approach wherever I can. For example, I think it’s much cleaner to implement underscoreToCamelCase like this:
func underscoreToCamelCase(string: String) -> String {
let items = string.componentsSeparatedByString(“_”)
return items.reduce(“”) { $0.isEmpty ? $1 : $0 + $1.capitalizedString }
}
No mutable variables, no loops, two lines.
awesome solution
This is awesome. I always knew there should be an easier way, but I never went to the trouble to implement something like this. I just discovered this site, and I’ll keep an eye on it for sure!
One question though: how would you handle more complicated json’s? Especially with nested objects. You would still need some manual work, right?
1. make fromJson recursive
2. add a way to make the loading custom for each class
Awesome solution, I love it
But I still have a question, it appear that if your property is an optional, it won’t always work.
More specifically, it will work with optional String but not with optional Int (I didn’t test other types yet). Any idea why it’s acting like that ? – I love optionals and kinda use them very often.
Anyway, thanks for all your explanations about the tricky little swift things
Actually, the night helping, I realised that’s it’s not suppose to work with optionals since they are not exposed to the Objective-C runtime and so “class_copyPropertyList” does not know about them :/
Too bad, but it might be an other way of doing it.
But still, if optionals are not exposed to the Objective-C runtime, why is it working with optional Strings ?
Instead of using class_copyPropertyList you could use Mirror for also getting the optional properties.
If you are looking for a complete reflection solution, then have a look at: https://github.com/evermeer/EVReflection/tree/Swift2
It also has workarounds for handling other Swift limitations.
I know this is older, but just saw it. I am a bit concerned about the security implications of using the keys of a dictionary received over the network to control which properties/ivars get set.
We basically have two conditions we have to meet. One is that the object needs to respond (have a method or a property) that is the same name as a key in the JSON dictionary. The second condition is that there has to be a setName method, a property named name, or a _name ivar.
In the User example, we really can’t set anything, but we can cause a crash. We can provide a JSON element for “asJson” or for “propertyNames”, or any NSObject method that doesn’t take any arguments. That will meet condition one. Condition two is much harder to meet, and I don’t see anything too interesting in User. But using a zero argument method name as a key will cause us to pass the check to see if our object responds to that selector. Since we don’t have a corresponding set, property, or _ ivar, we get a NSUnknownKeyException exception. Depending on your error handling, this could be a denial of service type attack.
A quick and dirty example of something more serious is if we change the api slightly and change asJason to json, and provide a setJason method (say you want to cache the json or whatever):
extension NSObject {
…
func json() -> NSDictionary {
…
}
func setJson(json : String) {
println(“Did you really mean this!!!”)
}
And you use this “json”:
let info = [
“firstName”: “Andrei”,
“lastName”: “Puni”,
“age”: 23,
“json”:”foobar!”
]
The code will call the setJson() method, which is probably not something we really want to do.
I think the code restoring from json should have a whitelist of properties that is allowed to be set from json. I know it is not as nice as looking up all the properties dynamically, but this just seems dangerous to me.
True. The load method should iterate trough the list of properties instead of the keys from the JSON.