Code: Update objects like a pro in Swift (3 of 3)

Part 3 of 3

RNDM
7 min readJun 7, 2017

How we got to here

The last two tutorials have allowed us to create some pretty cool and intense code that allows us to clone and update an object. If you’ve been following along so far, then your code will probably look something like this:

protocol Updateable { }extension Updateable where Self: NSObject {
@discardableResult
func update(completion: (Self) -> Void) -> Self {
completion(self)
return self
}
}
protocol Clonable {
init()
}
extension NSObject: Clonable { }extension Clonable where Self: NSObject {
var clone: Self {
let clone = Self.init()
for property in properties {
guard case let name = property.name,
let v = value(forKey: name)
else { continue }
clone.setValue(v, forKey: name)
}
return clone
}
}
extension UnsafeMutablePointer {
func properties(length: Int) -> [OpaquePointer] {
return UnsafeBufferPointer(
start: self,
count: length).flatMap { $0 as? OpaquePointer}
}
}
extension objc_property_t {
var name: String {
guard let name = property_getName(self),
let string = String(utf8String: name)
else { return "" }
return string
}
}
extension NSObject {
static var properties: [objc_property_t] {
guard let classForKeyedArchiver = classForKeyedArchiver()
else { return [] }
var count: UInt32 = 0
var properties = class_copyPropertyList(
classForKeyedArchiver,
&count).properties(length: Int(count))
if let parent = class_getSuperclass(classForKeyedArchiver)
as? NSObject.Type {
properties.append(contentsOf: parent.properties)
}
return properties
}
var properties: [objc_property_t] {
return type(of: self).properties
}
}

Unfortunately, we have left this bit of code in a non-running state due to the attributes of the object’s properties not being respected. So let’s set about making this right.

WARNING: Like the last tutorial, this one can go into depth pretty quickly. However, this one is not as deep in the bowels of code as the second part. Here we will cover some enums, and the different types of enums we can have, as well as some extensions. Thankfully, though, we will not be drilling into deep discoveries of Pointers too much… just acknowledging that they exist and can be recognised. Hope you enjoy!

Oh! Enums!

In the last tutorial, I mentioned that we are going to make our accessing of the attributes type safe. What better way to do this, than through enums? Well, for this, we are even going to have enums within enums! Whoop!

But before we do this, let’s first get access to the property attributes. In our extension on objc_property_t let’s grab these as an array of strings for our properties:

extension objc_property_t {
...
var attributes: [String] {
guard let att = property_getAttributes(self),
let string = String(utf8String: att),
case let array = string.components(separatedBy: ",")
else { return [] }
return array
}
}

When we print this out during our loop we get something that looks like this:

["T@\"NSString\"", "N", "C", "VfirstName"]
["T@\"NSString\"", "N", "C", "VlastName"]
["Tq", "N", "Vage"]

So what does all this mean? Apple has great documentation outlining all this, which you can read through here. But the short explanation is that we have a type (“T…”), some attributes in the middle and a description (“V…”). We are going to work a little with the types (which we will call a classification so as not to confuse the compiler), a little with the attributes and not at all with the description. But you can take this code and work further with the description of you want to.

Our first step is to create an enum called Attribute:

enum Attribute {
case readonly
case copy
case retain
case nonatomic
case dynamic
case weak
case garbage
case old
case customGetter
case customSetter
}

You’ll notice that I haven’t defined the type here. This is because I don’t want to make use of the rawValue or the initialiser, but define these myself. We’ll get to why a little later. Now we will add in a computed property to give us the value:

enum Attribute {
...
var value: Character {
switch self {
case .readonly: return "R"
case .copy: return "C"
case .retain: return "&"
case .nonatomic: return "N"
case .dynamic: return "D"
case .weak: return "W"
case .garbage: return "P"
case .old: return "t"
case .customGetter: return "G"
case .customSetter: return "S"
}
}

Now since we have the value, we should add a failable initialiser to return an attribute if one can be initialised by using a string.

enum Attribute {
...
init?(string: String) {
guard let first = string.characters.first
else { return nil }
switch first {
case Attribute.readonly.value: self = .readonly
case Attribute.copy.value: self = .copy
case Attribute.retain.value: self = .retain
case Attribute.nonatomic.value: self = .nonatomic
case Attribute.dynamic.value: self = .dynamic
case Attribute.weak.value: self = .weak
case Attribute.garbage.value: self = .garbage
case Attribute.old.value: self = .old
case Attribute.customGetter.value: self = .customGetter
case Attribute.customSetter.value: self = .customSetter
default: return nil
}
}

Since we will care only about our attributes, let’s just update our previous extension on objc_property_t to generate an array of Attribute.

extension objc_property_t {
...
var attributes: [Attribute] {
guard let att = property_getAttributes(self),
let string = String(utf8String: att),
case let array = string.components(separatedBy: ",")
else { return [] }
return array.flatMap{Attribute(string: $0)}
}
}

The flatMap will filter out any nil values and only return the items we want. However, the observant among you will notice that although we are passing back the attributes in this, we do not touch the type (Classification). This is because this one is a little more complex.

First, we will create an enum for our Classifications:

enum Classification: Character {
case char = "c"
case double = "d"
case float = "f"
case int = "q"
case unsignedint = "Q"
case long = "l"
case short = "s"
case id = "@"
case function = "?"
case structor = "}"
case void = "v"
case selector = ":"
init?(string: String) {
guard let last = string.characters.last else { return nil }
self.init(rawValue: last)
}
}

We’ve put together a pretty decent list here and most items are covered, but if I’ve missed any, don’t stress. This is more for example than for utilisation and if you find any missing ones, then it should be fairly simple to add in (there’s some homework for you!). Also, the struct is a little bit of a hack, just because of the way the type is created (i.e. T{ImaStruct=”a”i”b”c}), so I’m just using the last curly brace at this time.

Having done this, what we want to understand is that there are two distinct referencing systems (that I have observed) pointers and non-pointers. So we will create an enum to capture this as well.

enum Reference {    case standard(Classification)
case pointer(Classification)
init?(string: String) {
guard let type = Classification(string: string)
else { return nil }
if string.characters
.contains(Reference.pointer(.int).value) {
self = .pointer(type)
}else {
self = .standard(type)
}
}
var value: Character {
switch self {
case .pointer: return "^"
case .standard: return Character("")
}
}
}

As you can see we have put a reference into the Classification enum in both the standard and the pointer type. There is one more item that we will put in as a variable:

enum Reference {
...
var isObject: Bool {
var val: Classification
switch self {
case .pointer(let item): val = item
case .standard(let item): val = item
}
switch val {
case .function, .void, .selector: return false
default: return true
}
}
}

We will use this momentarily to determine if we should add this to our list of accessible properties through another computed property on our Attribute enum:

enum Attribute {
...
case type(Reference)
init?(string: String) {
...
switch first {
...
case Attribute.type(.standard(.int)).value:
guard let split = string
.components(separatedBy: "\"").first,
let type = Reference(string: split)
else { return nil }
self = .type(type)
...
}
}
var value: Character {
switch self {
...
case .type: return "T"
}
}
var canSet: Bool {
switch self {
case .readonly: return false
case .type(let item): return item.isObject
default: return true
}
}
}

Wow! What on earth are we doing here? Well, it’s not as painful as it seems. We are simply adding a new case into our Attribute enum and giving it a value. Then we are asking if the attribute is readonly or if it is an object so we know if we can set it. Simples…

We have done a fair amount of enum work now, so let’s put this to good use by extending Array where the element is Array:

extension Array where Element == objc_property_t {
var updatable: [objc_property_t] {
return filter{$0.attributes.filter({!$0.canSet}).count == 0}
}
}

And change our clone variable to only access the updatable properties:

var clone: Self {
let clone = Self.init()
for property in properties.updatable {
guard case let name = property.name,
let v = value(forKey: name)
else { continue }
clone.setValue(v, forKey: name)
}
return clone
}

Aaaaand… (drum roll)…. we’re done! We now have a fully functioning system for grabbing an instance of the object and then updating inline. Let’s give it a go.

class Person: NSObject {
var firstName: String = ""
var lastName: String = ""
var age: Int = 0
}
let person = Person()
person.update {
$0.firstName = "Paul"
$0.lastName = "Napier"
$0.age = 21 // Again...
}
let person2 = person.clone.update {
$0.age = 100
}
print(person.firstName)
print(person.lastName)
print(person.age)
print(person2.firstName)
print(person2.lastName)
print(person2.age)
/* Output
Paul
Napier
21
Paul
Napier
100
*/

Homework!

Though this is some pretty cool code, it has some limitations. For instance, we are only really creating a shallow clone. This means that if our object has a reference to another object, then when we create a clone we will not generate a new instance of that reference object. Here is an example that might help explain the concept:

We create a class called a Clone and we give it a variable of a type called Weapon, then we send them in to battle. When we build our army using the clone command, then it is important to know whether the weapon is a class or a struct (reference or value type). If it is a struct, then the battle will be as expected, but if it is a class, then our clones will be sent into battle sharing one weapon! Oops!

So take it away and do some homework… maybe one of you will come up with an awesome solution!

I hope you enjoyed these tutorials and it gives you something to really get your teeth into. There will be more to come over the next few weeks as well as a bunch of videos from the talks I am giving. For those who prefer to see this in action, you can find a playground here.

Good luck and happy coding!!

--

--