Skip to content
Dr. Mickey Lauer edited this page Sep 7, 2020 · 19 revisions

The Basics

You can store any kind of item you want using YapDatabase!

In order to store an object to disk (via YapDatabase or any other protocol) you need some way of serializing the object:

  • Serialization (Encoding): Convert in-memory object to blob of bytes
  • Deserialization (Decoding): Convert blob of bytes to in-memory object

With YapDatabase, you can customize the serialization per-collection. This makes it easy to store any type of class/struct in the database.

 

Swift: Codable protocol

Swift includes the Codable protocol, which makes encoding/decoding really easy:

There are only 2 things you need to do to store your custom classes/structs in YapDB:

  • Add Codable protocol support to your class/struct
  • Register the class/struct for the desired collection
class Foo: Codable { // Just implement Codable !
	// ...
}
struct Bar: Codable { // Just implement Codable !
}

db.registerCodableSerialization(Foo.self, forCollection: "foos")
db.registerCodableSerialization(Bar.self, forCollection: "bars")

Many built-in Swift types already support Codable, so you can store them directly:

db.registerCodableSerialization(String.self, forCollection: "recentQueries")

Note: If you get a runtime error similar to *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x6000032e01c0', it's because you tried to store a Swift struct that has not been registered before.

 

More Details

YapDatabase has various functions for configuring encoding/decoding:

// YapDatabase functions:

func registerDefaultSerializer(_ serializer: @escaping YapDatabaseSerializer)
func registerDefaultDeserializer(_ deserializer: @escaping YapDatabaseDeserializer)

func registerSerializer(_ serializer: @escaping YapDatabaseSerializer, forCollection collection: String?)

func registerDeserializer(_ deserializer: @escaping YapDatabaseDeserializer, forCollection collection: String?)

When you call registerCodableSerialization, it automatically creates both a serializer & deserializer for your class/struct (using Codable protocol), and registers them with the database for the specified collection.

If you want more control, you can just write your own serializer & deserializer, and register them instead. They're fairly straightforward:

typealias YapDatabaseSerializer = (collection: String, key: String, item: Any) -> Data

typealias YapDatabaseDeserializer = (collection: String, key: String, data: Data) -> Any?

Truth be told, YapDatabase doesn't care how you go about serializing/deserializing your objects. Just so long as you get the job done. After all, they're your objects, so you should have complete control over them.

 

Upgrades

Sooner or later, you're going to make changes to your class/struct. Luckily, it's easy to accomplish this, thanks to the Codable protocol.

For example, let's say we have a simple struct. And after shipping our app, we decide we need to add a new property to the struct: var age: UInt

The problem is that existing customers are going to have serialized versions of the OLD struct in the database. No problem! We can upgrade on the fly:

struct Foobar: Codable {
	
  enum CodingKeys: String, CodingKey {
    case version = "version" // Add this, to make versioning easier
    case name = "name"
    case age = "age" // We added this in our recent upgrade
  }
  
  private let version: Int = 1
  
  public var name: String
  public var age: UInt
  
  init(name: String, age: UInt) {
    self.name = name
    self.age = age
  }
  
  init(from decoder: Decoder) throws {
    
    let values = try decoder.container(keyedBy: CodingKeys.self)
    let version = try values.decodeIfPresent(Int.self, forKey: CodingKeys.version) ?? 0
    
    name = try values.decode(String.self, forKey: CodingKeys.name)
    
    // We added the 'age' property in version 1
    if version == 0 {
      age = 18 // use default age
    } else {
      age = try values.decode(UInt.self, forKey: CodingKeys.age)
    }
  }
}

 

Objective-C: NSCoding protocol

The default serializer & deserializer use NSCoding. So Objective-C developers only have to implement the NSCoding protocol for their classes.

NSCoding is an Apple protocol used to serialize/deserialize an object, and its already supported by a lot of the classes you're already using:

  • NSString
  • NSNumber
  • NSArray
  • NSDictionary
  • NSSet
  • NSData
  • UIColor
  • UIImage
  • etc...

So if you want to store objects in the database that already support NSCoding (such as strings, numbers, etc), then you don't have to do a thing. You're good to go.

Otherwise all you have to do is make sure your custom objects implement the 2 NSCoding methods.

@interface MyObject : NSObject <NSCoding>
@end

@implementation MyObject
{
  NSString *myString;
  UIColor *myColor;
  MyWhatever *myWhatever;
  float myFloat;
}

- (id)initWithCoder:(NSCoder *)decoder // NSCoding deserialization
{
  if ((self = [super init])) {
    myString = [decoder decodeObjectForKey:@"myString"];
    myColor = [decoder decodeObjectForKey:@"myColor"];
    myWhatever = [decoder decodeObjectForKey:@"myWhatever"];
    myFloat = [decoder decodeFloatForKey:@"myFloat"];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder // NSCoding serialization
{
  [encoder encodeObject:myString forKey:@"myString"];
  [encoder encodeObject:myColor forKey:@"myColor"];
  [encoder encodeObject:myWhatever forKey:@"myWhatever"];
  [encoder encodeFloat:myFloat forKey:@"myFloat"];
}

Not exactly rocket science is it? But what about that MyWhatever ivar?

It's simple. If MyWhatever supports NSCoding, then it's initWithCoder/encodeWithCoder method will be called. And everything just works.

It works the same way with arrays. If you have an NSArray of MyObject's, then you can just pass the array to the database. The initWithCoder/encodeWithCoder method of NSArray calls down to the corresponding method of its objects. So you can have arrays (or dictionaries or sets or whatever) that contain your custom objects. And your custom objects can have their own object graph.