Houzz Xcode plugin
Zcode is an Xcode plugin designed to prevent human errors when implementing multiple protocols and generating code.
- Download latest Zcode package from the Releases.
- Copy Zcode to your Applications folder.
- Launch Zcode once. You can close it immediately afterwards.
- Go to System Preferences > Extensions > Xcode Source Editor > select Zcode Note: If you trying to upgrade/downgrade ZCode and your XCode doesn't pick it from the Applications folder, it means there are other instances of Zcode somewhere on your mac. Run "pluginkit -m -A -v | grep com.houzz.Zcode" and remove them.
Assert IBoutlets will detect all @IBOutlets in a class, and create an assert statement for each and one of them in viewDidLoad()
or awakeFromNib()
accordingly.
In case you forgot to implement viewDidLoad()
or awakeFromNib()
(depends on the context) - an Xcode warning will appear
Running this command will generate the following code
class Demo: UIViewController {
@IBOutlet var test1: UILabel!
@IBOutlet var test2: UILabel!
@IBOutlet var test3: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Begin outlet asserts
assert(test1 != nil, "IBOutlet test1 not connected")
assert(test2 != nil, "IBOutlet test2 not connected")
assert(test3 != nil, "IBOutlet test3 not connected")
// End outlet asserts
}
}
Cast is one of the most popular commands in Zcode. Cast is used when conforming to DictionaryConvertible
protocol.
All you have to do is declare the class or struct, inherit from NSObject
and conform the DictionaryConvertible
protocol, like so:
class Demo: NSObject, DictionaryConvertible {
let id: Int
let fullName: String?
}
Of couse this code will generate compilation errors, but after running Zcode's Generate: Cast
command, it will look like this:
class Demo: NSObject, DictionaryConvertible {
let id: Int
let fullName: String?
func dictionaryRepresentation() -> [String: Any] { // Generated
var dict = [String: Any]()
dict["Id"] = id.jsonValue
dict["FullName"] = fullName?.jsonValue
// Add custom code after this comment
return dict
}
required init?(dictionary dict: JSONDictionary) { // Generated
if let v: Int = dict.value(for: "Id") {
id = v
} else {
LogError("Error: Demo.id failed init")
assert(false, "Please open API ticket if needed")
return nil
}
fullName = nilEmpty(dict.value(for: "FullName"))
super.init()
if !awake(with: dict) {
return nil
}
}
}
To enhance the power of Cast, you can apply your customization using special Zcode comments, maked with the prefix //!
Given the following demo JSON:
{
"id": 1,
"user": {
"fullName": "John Appleseed",
"nickname": "Johnny Boi",
"birthday": 719487660000,
"isAssigned": true
},
"hobby": "Watching Squid Game on Netflix"
}
Here are the list of Zcode comments you can use to suit your needs:
//! "User/FullName"
- this comment will populate your property based on this path, accessing nested objects in the JSON hierarchy.//! msec "User/Birthday"
- At Houzz, we represent dates in milliseconds, so addingmsec
to your Zcode comment will make sure to represent the Date in milliseconds.=
- Equal sign defines a default value.
For example, if we're parsing a nullable value, but don't want it to be null, use equal sign with your default value:
let name: String //! = "Anonymous" "User/FullName"
??
- This operator can help use another property as a fallback value, and can be nested multiple times.
let name: String //! = "Anonymous" "User/FullName ?? User/Nickname"
//! ignore
- This comment will tell Zcode to ignore everything regarding this property, and you will have to add your implementation manually after generating the code.//! ignore json
- This comment will prevent Zcode from handling this property indictionaryRepresentation()
andnit?(dictionary dict: JSONDictionary)
but it will be included in different protocols such as NSSecureCoding, Codable, etc..//! custom
- Will automatically call a static parsing function called parse - which you will have to implement manually.- Top of the file comments:
8.1
//! zcode: case camelCase
- will parse the JSON properties in camelCase 8.2//! zcode: case CamelCase
- will parse the JSON properties in CamelCase 8.3//! zcode: case screamingSnake
- Will parse the JSON properties in screaming snake case (mix of upper case and snake case). e.gPROJECT_OWNER
8.4//! zcode: emptyIsNil on/off
- Will treat empty strings as nil 8.5//! zcode: logger off
- Zcode generates a unique fingerprint based on the current code, if you change your property types or modify the class, the fingerprint will update after you run the Cast command.
9.1 Zcode contains a pre-commit hook, and it make sure that you ran the cast command after modifying your entity. If you forget to run Cast, your code will not be committed and a notification will be shown
If you'd like to add some custom behavior prior initialization of your entity, you should implement awake(with dictionary: JSONDictionary)
and return true/false representing if this property should be initialized or not
class Demo: NSObject, DictionaryConvertible {
let dueDate: Date
func dictionaryRepresentation() -> [String: Any] { // Generated
var dict = [String: Any]()
dict["DueDate"] = dueDate.jsonValue
// Add custom code after this comment
return dict
}
required init?(dictionary dict: JSONDictionary) { // Generated
if let v: Date = dict.value(for: "DueDate") {
dueDate = v
} else {
LogError("Error: Demo.dueDate failed init")
assert(false, "Please open API ticket if needed")
return nil
}
super.init()
if !awake(with: dict) {
return nil
}
}
func awake(with dictionary: JSONDictionary) -> Bool {
dueDate < Date() // Due date has not passed yet
}
}
We'd like to parse a Person
object with data based from the JSON above. We can use the power of Zcode comments to our advantage:
class Person: NSObject, DictionaryConvertible {
let id: Int
let name: String //! = "Anonymous" "User/Name ?? User/Nickname"
let birthday: Date? //! msec "User/Birthday"
let age: String //! ignore
func dictionaryRepresentation() -> [String: Any] { // Generated
var dict = [String: Any]()
dict["Id"] = id.jsonValue
var dict1 = dict["User"] as? [String: Any] ?? [String: Any]()
dict1["Name"] = name.jsonValue
if let birthday = birthday {
dict1["Birthday"] = Int(birthday.timeIntervalSince1970 * 1000)
}
dict["User"] = dict1
// Add custom code after this comment
dict["age"] = "29" // This line was added manually
return dict
}
required init?(dictionary dict: JSONDictionary) { // Generated
if let v: Int = dict.value(for: "Id") {
id = v
} else {
LogError("Error: Person.id failed init")
assert(false, "Please open API ticket if needed")
return nil
}
name = nilEmpty(dict.value(for: "User/Name")) ?? nilEmpty(dict.value(for: "User/Nickname")) ?? "Anonymous"
birthday = dict.value(for: "User/Birthday")
age = "29" // This line was added manually
super.init()
if !awake(with: dict) {
return nil
}
}
}
Similar to Cast, Cast read will only generate a func read(from dict: JSONDictionary)
function.
The content of the function is also based on the entity's properties, similar to Cast.
Cast read will only populate variable fields.
Following our previous example:
class Person: NSObject, DictionaryConvertible {
var name: String //! = "Anonymous" "User/FullName"
func read(from dict: JSONDictionary) { // Generated
if let v: String = nilEmpty(dict.value(for: "User/FullName")) {
name = v
}
// Add custom code after this comment
}
}
NSCoding will generate code that conforms your entity to NSCoding protocol, based on your properties.
Note: Zcode comments will not affect this code generation command
class Person: NSCoding {
var name: String
required init?(coder aDecoder: NSCoder) { // Generated
if let v = String.decode(with: aDecoder, fromKey: "name") {
name = v
} else {
return nil
}
// Add custom code after this comment
}
func encode(with aCoder: NSCoder) { // Generated
name.encode(with: aCoder, forKey: "name")
// Add custom code after this comment
}
}
NSCopying will generate code that conforms your entity to NSCoding protocol, based on your properties.
To use this command, your entity must conform to NSCoding
and NSCopying
protocols.
Note: Zcode comments will not affect this code generation command
class Person: NSCoding, NSCopying {
var name: String
func encode(with aCoder: NSCoder) { // Generated
name.encode(with: aCoder, forKey: "name")
// Add custom code after this comment
}
required init?(coder aDecoder: NSCoder) { // Generated
if let v = String.decode(with: aDecoder, fromKey: "name") {
name = v
} else {
return nil
}
// Add custom code after this comment
}
func copy(with zone: NSZone? = nil) -> Any { // Generated
let aCopy = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [Person.self], from: NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true))!
// Add custom code after this comment
return aCopy
}
}
This command creates an initializer for your entity, based on it's properties. You can use the default value Zcode comments from Cast to define default values in the initializer.
class Person {
let id: Int //! = 0
let name: String? //! = "Anonymous"
let birthday: Date
init(id: Int = 0, name: String? = "Anonymous", birthday: Date) { // Generated Init
self.id = id
self.name = name
self.birthday = birthday
// Add custom code after this comment
}
}
To use Make Defaults, your entity must inherit from UserDefaults
, if you forget to do so, an Xcode warning will be shown:
This command parses a list of DefaultKey and creating a property for each of these keys in a separate extension (generated).
Each generated property will have the getter and setter.
It's recommended (but not mandatory) to use a static var called allKeys
which is an array of DefaultKey
to declare about all of your keys.
class DemoDefaults: UserDefaults {
static let allKeys: [DefaultKey] = [
DefaultKey("id", type: .int, options: [.objc, .write]),
DefaultKey("name", type: .string, options: [.objc, .write]),
DefaultKey("isAssigned", type: .bool, options: [.objc, .write])
]
}
// MARK: - Generated accessors
extension DemoDefaults {
@objc public var id: Int {
get {
return integer(forKey: "id")
}
set {
set(newValue, forKey: "id")
}
}
@objc public var name: String? {
get {
return object(forKey: "name") as? String
}
set {
set(newValue, forKey: "name")
}
}
@objc public var isAssigned: Bool {
get {
return bool(forKey: "isAssigned")
}
set {
set(newValue, forKey: "isAssigned")
}
}
}
This command generates encode(from encoder: Encoder)
, init(from decoder: Decoder)
and CodingKeys
enum.
Your entity must conform to the Codable protocol
class Person: Codable {
let name: String
func encode(to encoder: Encoder) throws { // Generated
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
// Add custom code after this comment
}
required init(from decoder: Decoder) throws { // Generated
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeString(forKey: .name)
// Add custom code after this comment
}
private enum CodingKeys: String, CodingKey { // Generated
case name
// Add custom code after this comment
}
}
This command will override the relevant methods used to support restore state, which are available if your class subclass UIViewController
.
class DemoViewController: UIViewController {
private enum CodingKeys: String, CodingKey {
case <#key#>
}
override open func saveState(to encoder: Any) throws {
guard let encoder = encoder as? Encoder else { return }
var container = encoder.container(keyedBy: CodingKeys.self)
<#Encode view controller state here#>
try super.saveState(to: encoder)
}
override open func restoreState(from decoder: Any) throws {
guard let decoder = decoder as? Decoder else { return }
let container = try decoder.container(keyedBy: CodingKeys.self)
// View is not yet loaded, insert _ = view if need to load view
<#Decode view controller state here#>
try super.restoreState(from: decoder)
}
// return a view controller, don't call restore on it, if this is a superclass that is not supposed to be saved
// directly, can omit implementing this function if shouldSaveState return false
open override class func viewController(using decoder: Any) throws -> UIViewController {
guard let decoder = decoder as? Decoder else { throw SaveStateError(.notStateDecoder) }
<#Create view controller here#>
}
// Return if we should save state on this controller (can change during controller lifetime)
open override var shouldSaveState: Bool { true }
}
- update
Config.xcconfig
with the new build and config numbers - create a new tag with the version name
- on your local machine, archive the zcode project
- select "Distribute" using "Development" option
- Make sure your Xcode is named exactly
Xcode.app
and not something likeXcode14.app
- Make sure zcode is enabled in the extensions manager