/CodeGen-Workshop

This is the repository used for my SwiftAveiro'19 Workshop about Code Generation

Primary LanguageSwift

Code Generation Workshop

Learn to use SwiftGen & Sourcery to avoid boilerplate and improve your productivity. CodeGen also allows you to provide more type safety & make your code maintenance!

Requirements

  • This workshop expects that you already know how to write Swift 4+ code and have written a few (ideally iOS) applications.
  • We'll use Xcode 10.2, but older Xcodes should work too

Resources

During the workshop, we'll use the following websites


Workshop Steps

Stage 0: Discover the project

Note: This project is in no way intended to be a model of good architecture 😅 It has been crafted specifically to feature as many possible use cases for Code Generation as possible while trying to keep the app as simple as possible. 😉

  • Browse the source code quickly
  • Compile and launch the project in Xcode – ow, it crashes, what a good start!
  • Identify the issues with hardcoded values and crashes:
    • Localization: wrong keys, untranslated 🚫
    • Images: wrong keys, crash 💥
    • Fonts: wrong name, some missing in Info.plist, fallback to system font 🚫
    • Storyboards: hardcoded Storyboard and Scene names, crash 💥
    • Models: Lots to data to type by hand ⌨️
    • Info.plist : hardcoded keys 🚫
    • Lottie: hardcoded name, one animation not found 🚫
  • Identify the annoying repetition of code
    • Big switches just to identify a name property
    • Image associated with Item should be derived from item case
    • Equatable implementations for enums
    • ...

Stage 1: Discover SwiftGen and its Build-in templates

  • Install SwiftGen via CocoaPods (†)
  • Read the doc about the Config File format, play with swiftgen --help and swiftgen templates list
  • Make your first swiftgen.yml and generate constants for xcassets via the command line
  • Fix the code that is using images and colors
    • Add the generated file to your project
    • Start using the generated constants in the code, see that some are missing
  • Install SwiftGen as a Build Phase in your project so that you get constant updates
    • Add InputData/person@3x.png and InputData/planets@3x.png to your Assets Catalog and rebuild to see new constants.
  • Continue with all the other types of resources
    • Fonts to swiftgen.yml – Don't forget to remove them from the Info.plist too!
    • Storyboards and Scene names
    • NSLocalizedString
    • Hardcoded CFBundleName from Info.plist
  • Bonus: Play a bit by renaming your image assets or Storyboard IDs and see how the changes are now warning you at compile time about where to change their references

(†) Alternatively you can download the ZIP file for SwiftGen and unzip it in your repo.

Stage 2: Custom SwiftGen Templates

  • Discover Stencil doc
  • Create a Lottie template to list all animations, add generated code to Xcode, replace call sites to fix the wrong animation name. In addition to generate the name of JSON files, also try to extract the width and height of the animations from the JSON content.
  • Create templates for YAML model files (see InputData/), in order to generate all the model objects (replacing the content of files in Generated/

Stage 3: Discover Sourcery

  • Install Sourcery via CocoaPods (†)
  • Install it as a Build Phase in your project
  • Take a look at its README and online documentation
  • Use the AutoEquatable template and make it work on the Item type.
    (You might want to duplicate and customize the template to remove the public access modifier) 😉
  • Bonus: Use Sourcery Annotations // sourcery:skipEquality to exclude some fields from AutoEquatable on models (e.g. id)

(†) Alternatively you can download the ZIP file for Sourcery and unzip it in your repo.

Stage 4: Custom Sourcery Templates

  • Create Custom template listing all Model types. Use the --watch mode to iterate
  • Make the template evolve to re-generate the enum Item based on those Model-conforming types
  • Add the generation of the image and description properties and the Encodable conformance
  • Add a custom annotation to identify the name property and generate the var name from it
  • Make the func item() -> Item in extension ID/ID.swift be generated by Sourcery instead

Stage 5: Generate all fields for all items

  • Make Item+Fields.swift be generated by Sourcery
  • Use a custom annotation to exclude the openingCrawl property from the fields
  • Make ItemStore.swift and its subscript and static let filters be generated by Sourcery

Stage 6: Add and rename models and see the magic happen

  • Mark the Planet to conform to Model and change Person.homeworld: ID<Planet> and Film.planets: [ID<Planet>]
  • Check that rebuilding – and thus re-running Sourcery – should now update all the places where we depend on Item cases (ItemStore.generated.swift, Item+Fields.generated.swift, Item.generated.swift)
  • Change your template for Item+Fields.generated.swift to generate person.homeworld.displayName

Stage 7: Rename things, Ignore properties for Encodable

  • Rename struct Film to struct Movie and every ID<Film> into ID<Movie>, then ensure everything changes automatically in the rest of the code too.
    Notice that the compiler will prompt us to rename the corresponding image too. Rename it and see SwiftGen re-generate the proper constants
  • Custom Encodable for Models, mark some properties like id or openingCrawl to be ignored

Stage 8: Inline code generation

  • Change your model types to be classes.
  • Since classes require an explicit init, you'll have to generate one... and it has to be declared in the original type implementation (you can't declare it in an extension). So we're gonna need to make Sourcery generate code inline, inside our existing code
  • Look at the Sourcery documentation about Writing Templates – see how to achieve that with special // sourcery:inline:... annotations, and make a template to generate that init code.
    • Tip: when looping over all your storedVariables, you can check forloop.last to see if you're on the last element of your list and omit the comma in that case.

Going Further