Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Code Generation in Swift — UIKonf'17

Code Generation in Swift — UIKonf'17

Swift is a great, type safe and powerful language. But some stuff are still cumbersome to write yourself. And some APIs could deserve more type-safety and less string-based methods.

In this talk we'll discover some popular tools to do code generation for Swift, especially Sourcery and SwiftGen. We'll see how they work, what you can do with them, some concrete and advanced examples of how your code can be made safer, and how you can save a lot of development time thanks to those tools.

AliSoftware

May 15, 2017
Tweet

More Decks by AliSoftware

Other Decks in Programming

Transcript

  1. @aligatr let format = NSLocalizedString("home.greetings", comment: "") String(format: format, 1

    , "UIKonf", "Berlin") "Berlin", 1, "UIKonf"? "UIKonf", "Berlin", 1? 1, "Berlin", "UIKonf"? 5
  2. @aligatr let format = NSLocalizedString("home.greetings", comment: "") String(format: format, 1

    , "UIKonf", "Berlin") Thread 1: EXC_BAD_ACCESS (code=1, address=0x1) "home.greetings" = "… %@ … %d … %@ …" 6
  3. @aligatr Code Generation swiftgen fonts "…/Fonts" … swiftgen strings "…/Localizable.strings"

    … swiftgen images "…/Assets.xcassets" … swiftgen storyboards "$PROJECT_DIR" … swiftgen colors "…/colors.xml" … … 11
  4. @aligatr Constants for free! enum FontFamily { enum Avenir: String

    { case black = "Avenir-Black" case blackOblique = "Avenir-BlackOblique" case oblique = "Avenir-Oblique" case roman = "Avenir-Roman" func font(size: CGFloat) -> UIFont! { return Font(font: self.rawValue, size: size) } } } 12
  5. @aligatr Constants for free! enum L10n { /// Welcome to

    %@, the #%d iOS conference in %@! case homeGreetings(String, Int, String) /// Dismiss changes? case editAlertTitle /// Changes have been made to the image « %@ » case editAlertMessage(String) /// Save case editAlertSave /// Dismiss case editAlertDismiss } 13
  6. @aligatr enum Assets { static let apple = UIImage(named: "Apple")

    static let banana = UIImage(named: "Banana") static let orange = UIImage(named: "Orange") static let somePears = UIImage(named: "Some-Pears") } 15 enum Assets: String { case apple = "Apple" case banana = "Banana" case orange = "Orange" case somePears = "Some-Pears" var image: UIImage { return UIImage(named: self.rawValue) } }
  7. @aligatr A Stencil Template enum Assets { {% for image

    in images %} static let {{image|swiftIdentifier}}
 = UIImage(named: "{{image}}") {% endfor %} } 17
  8. @aligatr A Stencil Template enum Assets: String { {% for

    image in images %} case {{image|swiftIdentifier|lowerFirstWord}} = "{{image}}" {% endfor %} var image: UIImage { return UIImage(asset: self) } } extension UIImage { convenience init!(asset: Assets) { self.init(named: asset.rawValue) } } 18
  9. @aligatr Profit! ✓ Free auto-completion + discovery (font names, …)

    ✓ Type-safety (typos, types in format strings, …) ✓ Customizable ✓ Generate anything (documentation, even ObjC ) ✓ No more maintenance 19
  10. @aligatr What about boilerplate code? struct ImageInfo: Equatable { let

    title: String let author: String let date: Date } func == (lhs: ImageInfo, rhs: ImageInfo) -> Bool { guard lhs.title == rhs.title else { return false } guard lhs.author == rhs.author else { return false } guard lhs.date == rhs.date else { return false } return true } 21
  11. @aligatr What about boilerplate code? struct ImageInfo: Equatable { let

    title: String let author: String let date: Date let cameraModel: String let kind: ImageKind } func == (lhs: ImageInfo, rhs: ImageInfo) -> Bool { guard lhs.title == rhs.title else { return false } guard lhs.author == rhs.author else { return false } guard lhs.date == rhs.date else { return false } return true } 22
  12. @aligatr There are {{types.all.count}} types in this code, including: -

    {{types.enums.count}} enums - {{types.structs.count}} structs - {{types.classes.count}} classes There are 22 types in this code, including: - 11 enums - 1 structs - 6 classes Introspect your code Template Generated Code 26
  13. @aligatr What do we want? Equatable! struct ImageInfo { let

    title: String let author: String let date: Date } Your code 27
  14. @aligatr What do we want? Equatable! extension ImageInfo: Equatable {}

    func == (lhs: ImageInfo, rhs: ImageInfo) -> Bool { guard lhs.title == rhs.title
 else { return false } guard lhs.author == rhs.author
 else { return false } guard lhs.date == rhs.date
 else { return false } return true } Generated Code we want 28
  15. @aligatr How do we want it? Automatically! {% for type

    in types.implementing.AutoEquatable %} extension {{ type.name }}: Equatable {} func == (lhs: {{ type.name }}, rhs: {{ type.name }}) -> Bool { {% for variable in type.storedVariables %}
 guard lhs.{{ variable.name }} == rhs.{{ variable.name }}
 else { return false } {% endfor %} return true } {% endfor %} Template 29
  16. @aligatr How do we want it? Automatically! struct ImageInfo :

    AutoEquatable { let title: String let author: String let date: Date } Your code $ sourcery/bin/sourcery + = 30
  17. @aligatr JSON Parsing struct Contact { let id: String let

    firstName: String let lastName: String } 31
  18. @aligatr JSON Parsing: Boooriiing! extension Contact: JSONDeserializable { init?(json: [String:

    Any]) { self.id = json["id"] as? String self.firstName = json["first_name"] as? String self.lastName = json["last_name"] as? String self.dateOfBirth = (json["dob"] as? String)
 .flatMap(JSONDateFormatter.date(from:)) self.avatar = (json["avatar"] as? [String: Any])
 .flatMap(Avatar.init(json:)) } } 32
  19. @aligatr Code Gen to the rescue! {% for type in

    types.implementing.AutoJSONDeserializable %} extension {{ type.name }}: JSONDeserializable { init?(json: [String: Any]) { {% for prop in type.storedVariables %} // ? {% endfor %} } } {% endfor %} 33
  20. @aligatr First attempt {% for prop in type.storedVariables %} self.{{prop.name}}

    = json["{{prop.name}}"] as? {{prop.typeName}} {% endfor %} self.id = json["id"] as? String self.firstName = json["firstName"] as? String self.lastName = json["lastName"] as? String 35
  21. @aligatr Sourcery Annotations! struct Contact { let id: String //

    sourcery: JSONKey = "first_name" let firstName: String // sourcery: JSONKey = "last_name" let lastName: String } {% for prop in type.storedVariables %} json["{{ prop.annotations.JSONKey |default:prop.name }}"] {% endfor %} 36
  22. @aligatr Sourcery Annotations! {% for prop in type.storedVariables %} self.{{prop.name}}

    = json["{{ prop.annotations.JSONKey |default:prop.name }}"] as? {{prop.typeName}} {% endfor %} self.id = json["id"] as? String self.firstName = json["first_name"] as? String self.lastName = json["last_name"] as? String 37
  23. @aligatr Save tons of code already! struct Customer: AutoEquatable, AutoJSONDeserializable

    {
 … // 15 properties
 } struct Product: AutoEquatable, AutoJSONDeserializable { … // 28 properties } struct Cart: AutoEquatable, AutoJSONDeserializable { … // 12 properties } struct Order: AutoEquatable, AutoJSONDeserializable { … // 17 properties } enum ShippingOption: AutoCases { … // 9 cases } 39
  24. @aligatr Type Erasure let list: [AnyPokemon<Thunder>] = … // sourcery:

    TypeErase = PokemonType protocol Pokemon { associatedtype PokemonType func attack(move: PokemonType) } 40 github.com/AliSoftware/SourceryTemplates + =
  25. @aligatr Going Further • Building an API Client using Sourcery

    Annotations • littlebitesofcocoa.com/295-building-an-api-client-with-sourcery- key-value-annotations • Generate JSON Parsing code • github.com/Liquidsoul/Sourcery-AutoJSONSerializable • Automatic Type Erasure • github.com/AliSoftware/SourceryTemplates 42
  26. @aligatr Going Further • Krzysztof’s talk at CraftConf about Sourcery

    • https://www.ustream.tv/recorded/102903026 • My Live Demo of SwiftGen at NSBudapest • Slides: speakerdeck.com/alisoftware • Video: http://www.ustream.tv/recorded/103135632 • Improve SwiftGen • We love contributors! ❤ (Free push access) 43