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.

Fb60bbce2fadcc41e950ef8dea3f29e5?s=128

AliSoftware

May 15, 2017
Tweet

Transcript

  1. @aligatr Slides & Code Snippets @aligatr tiny.cc/uikonf-codegen

  2. Code Generation in Swift SwiftGen & Sourcery May 14-17
 2017

    Olivier Halligon @aligatr 2 ✨
  3. @aligatr UIFont(name: "Avenir-It ", size: 42) Avenir-Italics? Avenir-Oblique? AvenirItal? Avenir.ttc

    __HelvN_BI.ttf 3
  4. @aligatr NSLocalizedString("home.gr ", comment: "") home.greeting? home.greetings? homeGreetings? 4

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

    , "UIKonf", "Berlin") "Berlin", 1, "UIKonf"? "UIKonf", "Berlin", 1? 1, "Berlin", "UIKonf"? 5
  6. @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
  7. None
  8. @aligatr Constants! label.text = L10n.homeGreetings("UIKonf", 1, "Berlin").string label.font = FontFamily.Avenir.blackOblique.font(size:

    18) 8
  9. @aligatr 9

  10. @aligatr Code Generation! SwiftGen ✨ 10

  11. @aligatr Code Generation swiftgen fonts "…/Fonts" … swiftgen strings "…/Localizable.strings"

    … swiftgen images "…/Assets.xcassets" … swiftgen storyboards "$PROJECT_DIR" … swiftgen colors "…/colors.xml" … … 11
  12. @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
  13. @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
  14. None
  15. @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) } }
  16. @aligatr Templates! 16

  17. @aligatr A Stencil Template enum Assets { {% for image

    in images %} static let {{image|swiftIdentifier}}
 = UIImage(named: "{{image}}") {% endfor %} } 17
  18. @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
  19. @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
  20. @aligatr

  21. @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
  22. @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
  23. @aligatr What about boilerplate code? 23

  24. @aligatr Sourcery! 24

  25. @aligatr MetaProgramming! Parser Internal Representation Stencil Template Generated Code Template

    Engine Your code (SourceKitten) 25
  26. @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
  27. @aligatr What do we want? Equatable! struct ImageInfo { let

    title: String let author: String let date: Date } Your code 27
  28. @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
  29. @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
  30. @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
  31. @aligatr JSON Parsing struct Contact { let id: String let

    firstName: String let lastName: String } 31
  32. @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
  33. @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
  34. @aligatr Live Templating daemon mode to write your templates live:

    $ sourcery/bin/sourcery --watch 34
  35. @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
  36. @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
  37. @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
  38. @aligatr 38

  39. @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
  40. @aligatr Type Erasure let list: [AnyPokemon<Thunder>] = … // sourcery:

    TypeErase = PokemonType protocol Pokemon { associatedtype PokemonType func attack(move: PokemonType) } 40 github.com/AliSoftware/SourceryTemplates + =
  41. @aligatr Why stop there? • Lenses • Decorators • Property-Based

    Testing (SwiftCheck) • … 41
  42. @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
  43. @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
  44. @aligatr Hello, I’m Olivier github.com/SwiftGen/SwiftGen github.com/krzysztofzablocki/Sourcery github.com/AliSoftware (OHHTTPStubs, Reusable, …)

    alisoftware.github.io @aligatr ✨ 44 speakerdeck.com/alisoftware