Skip to content

An iOS framework for creating JSON-based models. Written in Swift.

License

Notifications You must be signed in to change notification settings

ovenbits/ModelRocket

Repository files navigation

ModelRocket

Build Status Carthage compatible CocoaPods Compatible License Platform

An iOS framework for creating JSON-based models. Written in Swift (because it totally rules!)

Requirements

  • iOS 8.0+
  • Xcode 7.3
  • Swift 2.2

Installation

Embedded frameworks require a minimum deployment target of iOS 8

Carthage

Carthage is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application.

You can install Carthage with Homebrew using the following commands:

$ brew update
$ brew install carthage

To integrate ModelRocket into your Xcode project using Carthage, specify it in your Cartfile:

github "ovenbits/ModelRocket"

Then, run carthage update.

Follow the current instructions in Carthage's README for up-to-date installation instructions.

CocoaPods

CocoaPods is a dependency manager for Cocoa projects.

CocoaPods 0.36 adds supports for Swift and embedded frameworks. You can install it with the following command:

$ gem install cocoapods

To integrate ModelRocket into your Xcode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!

pod 'ModelRocket'

Then, run pod install.

Swift Package Manager

The Swift Package Manager is a dependency management tool provided by Apple, still in early design and development. For more infomation check out its GitHub Page.

You can use the Swift Package Manager to install ModelRocket by adding it as a dependency in your Package.swift file:

import PackageDescription

let package = Package(
    name: "PROJECT_NAME",
    targets: [],
    dependencies: [
        .Package(url: "https://github.com/ovenbits/ModelRocket.git", versions: "1.2.3" ..< Version.max)
    ]
)

Usage

Creating a custom object

class Vehicle: Model {
	let make  = Property<String>(key: "make")
	let model = Property<String>(key: "model", required: true)
	let year  = Property<Int>(key: "year") { year in
		if year < 2015 {
			// offer discount
		}
	}
	let color = Property<UIColor>(key: "color", defaultValue: UIColor.blackColor())
}

NOTE: As with all Swift variables, let should always be used, unless var is absolutely needed. In the case of Model objects, let should be used for all Property[Array|Dictionary] properties, as it still allows the underlying value to be changed, unless you truly need to reassign the property

Supported Types

  • String
  • Bool
  • Int
  • UInt
  • Double
  • Float

In addition to the core types above, ModelRocket also supports serialization for several other classes out of the box:

  • NSDate — ISO8601-formatted string (2015-05-31T19:00:17.000+0000)
  • UIColor — hex-color string (#f6c500)
  • NSURL — any url string (http://ovenbits.com)
  • NSNumber — any number, can be used in place of Double, Float, Int, and UInt

Creating an object with a typed array

// `Model` subclasses get `fromJSON` and `toJSON` implementations on `JSONTransformable` for free,
// but explicit `JSONTransformable` conformance is still required
extension Vehicle: JSONTransformable {}

class Vehicles: Model {
    let vehicles = PropertyArray<Vehicle>(key: "vehicles")
}

PropertyArray conforms to CollectionType, therefore, the .values syntax is not necessary when iterating through the values. For example:

let allVehicles = Vehicles(json: <json>)

// using `.values` syntax
for vehicle in allVehicles.vehicles.values {
}

// using `CollectionType` conformance
for vehicle in allVehicles.vehicles {
}

Creating an object with a typed dictionary

class Car: Vehicle {
	let purchasedTrims = PropertyDictionary<Int>(key: "purchased_trims")
}

PropertyDictionary conforms to CollectionType, therefore, the .values syntax is not necessary when iterating through the keys and values. For example:

let vehicle = Vehicle(json: <json>)

// using `.values` syntax
for (key, trim) in vehicle.purchasedTrims.values {
}

// using `CollectionType` conformance
for (key, trim) in vehicle.purchasedTrims {
}

NOTE: All object in the dictionary must be of the same type. If they're not, the app won't crash, but values of different types will be discarded

Initializing and using a custom object

// instantiate object
let vehicle = Vehicle(json: json)

// get property type
println("Vehicle make property has type: \(vehicle.make.type)")

// get property value
if let make = vehicle.make.value {
	println("Vehicle make: \(make)")
}

Model objects also contain a failable initializer, which will only return an initialized object if all properties marked as required = true are non-nil.

// instantiate object, only if `json` contains a value for the `make` property
if let vehicle = Vehicle(strictJSON: json) {
	// it's best to avoid implicitly unwrapped optionals, however, since `vehicle` is initialized iff `make` is non-nil, if can be force-unwrapped safely here
	println("Vehicle make: \(vehicle.make.value!)")
}
else {
	pintln("Invalid JSON")
}

Subclassing a custom object

class Car: Vehicle {
    let numberOfDoors = Property<Int>(key: "number_of_doors")
}

Adding a custom object as a property of another object

The custom object must conform to the JSONTransformable protocol by defining the following variables/functions

  • class func fromJSON(json: JSON) -> T?
  • func toJSON() -> AnyObject
class Vehicle: Model {
	let manufacturer = Property<Manufacturer>(key: "manufacturer")
}

class Manufacturer: Model {
    let companyName = Property<String>(key: "company_name")
    let headquarters = Property<String>(key: "headquarters")
    let founded = Property<NSDate>(key: "founded")
}

extension Manufacturer: JSONTransformable {
    class func fromJSON(json: JSON) -> Manufacturer? {
        return Manufacturer(json: json)
    }
    func toJSON() -> AnyObject {
        return self.json().dictionary
    }
}

Using an enum as a property

ModelRocket supports enum types for Property[Array|Dictionary] properties, as long as the enum conforms to the JSONTransformable protocol.

As a simple example, the material type of a vehicle's interior could use an enum like this:

enum VehicleInterior: String {
    case Fabric = "fabric"
    case Leather = "leather"
}

extension VehicleInterior: JSONTransformable {
    static func fromJSON(json: JSON) -> VehicleInterior? {
        return VehicleInterior(rawValue: json.stringValue)
    }
    func toJSON() -> AnyObject {
        return rawValue
    }
}

class Vehicle: ModelRocket {
   let interior = Property<VehicleInterior>(key: "interior")
}

Property postProcess hook

The Property postProcess closure (also available on PropertyArray and PropertyDictionary) provides a mechanism for work to be done after all properties of a Model object have been initialized from JSON but before the Model object has finished initializing.

class Vehicles: Model {
    let vehicles = PropertyArray<Vehicle>(key: "vehicles") { (values) -> Void in
        for vehicle in values {
            println("postHook vehicle: \(vehicle.make.value!)")
        }
    }
}

.value accessor usage pattern

A ModelRocket property is of type Property<T>. When accessing the property's value, you go through Property.value, e.g.:

let vehicleMake = make.value

It is perfectly acceptable to to utilize Propertys and access the property value directly. However, you may want a different public API for your model objects.

private let _make = Property<String>(key: "make")
public var make: String {
  get {
    return make.value ?? "unknown make"
  }
  set {
    make.value = newValue
  }
}

This usage pattern enables:

  • A cleaner public API
  • A public API that makes the type more proper: we expect "make" to be a string, not a Property
  • value is optional because it must be for general applicability, but your API may be more correct to have non-optional. Of course, if your API wants an optional, that's fine too.
  • The ability to process or convert the raw JSON value to other values more proper to your object's public API
  • Whereas a Property.value could be set, you could omit the set accessor for a read-only property, again helping to expose exactly the API for your object that you desire.
  • This usage of the bridge pattern enables ModelRocket to become an implementation detail and minimize dependencies and long-term maintenance.

Implementing a class cluster

Override the modelForJSON(json: JSON) -> Model function

class Vehicle: Model {
    let make = Property<String>(key: "make")
    let model = Property<String>(key: "model")
    let year = Property<Int>(key: "year")
    let color = Property<UIColor>(key: "color")
    let manufacturer = Property<Manufacturer>(key: "manufacturer")
    
    override class func modelForJSON(json: JSON) -> Vehicle {
        
        switch json["type"].stringValue {
        case "car":
            return Car(json: json)
        case "plane":
            return Plane(json: json)
        case "bike":
            return Bike(json: json)
        default:
            return Vehicle(json: json)
        }
    }
}

Then to access subclass-specific properties, use a switch-case

let vehicle = Vehicle.modelForJSON(vehicleJSON)
                
switch vehicle {
case let car as Car:
	// drive the car
case let plane as Plane:
	// fly the plane
case let bike as Bike:
	// ride the bike
default:
	// do nothing
}

Obtaining the object's JSON representation

Calling the json() function on a Model subclass returns a tuple containing:

  • dictionary: [String : AnyObject]
  • json: JSON
  • data: NSData

Obtaining a copy of a custom object

Call copy() on the object, and cast to the correct type. Example:

let vehicleCopy = vehicle.copy() as! Vehicle

License

ModelRocket is released under the MIT license. See LICENSE for details.