Holy Swift

Holy Swift

Making Documentation that is pleasant to read in Swift

Making Documentation that is pleasant to read in Swift

- Precondition: Be open minded about documentation

Subscribe to my newsletter and never miss my upcoming articles

Hallo meisjes en jongens, Leo hier.

Today we will explore a little further into how can you be more descriptive in your Swift code. A very important part of every developer day-to-day live is to write code that anyone will understand, specially because maybe you will be reading your code in the future and you sure want that you can reason about it.

There are some disclaimers that we should do before start this journey on documentation.

The first one is that no matter how good your documentation is your actual API and language feature implementations should always come first when reasoning about your code. The second disclaimer is always try to express yourself in your code, use documentation for everything that isn't possible to with a good naming or the naming would be too big/weird. And finally, documentation is your friend and tool so use it in the right situations.

Let's code but first...

The Painting

This painting is a 17 century masterpiece called St. Ambrose made by Matthias Stom. Matthias Stom or Matthias Stomer (c. 1600 – after 1652) was a Dutch, or possibly Flemish, painter who is only known for the works he produced during his residence in Italy. He was influenced by the work of non-Italian followers of Caravaggio in Italy, in particular his Dutch followers often referred to as the Utrecht Caravaggists, as well as by Jusepe de Ribera and Peter Paul Rubens.

I chose this painting because that is the face that I do when I'm reading documentation that are not complete/updated, or there's nothing to read at all. You all been in that same place, right fellows?

The Problem

You are developing a tricky framework and it's important for the users to know exactly what's going on under the hood.

First things first. Open your favorite playground file and copy paste the code below:

struct PrettyDocs {

    func sumValues(x: Int, y: [Int]) throws -> Int? {

        if x < 0 {
            throw MyError.Number
        }

        guard !y.isEmpty else { return nil }

        return y.reduce(0, +) + x
    }
}


let pd = PrettyDocs()
print(try pd.sumValues(x: 9, y: [1,2]))

The function is simple but has a lot to explore here. It can throw errors, and will return an Optional enum Integer.

The default documentation provided by Xcode 12.5 is:

Screenshot 2021-08-30 at 09.02.49.png

As you can imagine we will improve this to became a really beautiful piece of documentation.

Let start adding that to our method. But first let's explain that we have two ways to document Swift code: the single line and the multi-line.

The single line /// looks like this:

    /// This is function sum the X value with all the Y values and return Optional Int.
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

In this article we will focus in the multi-line one /** */:

    /**
     This is function sum the X value with all the Y values 
     and return Optional Int.
     */
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

Screenshot 2021-08-31 at 08.30.33.png

Summary Part

Take a look at the image above again. The Xcode added this Summary title to our documentation without we even need to explicit put that. This occurs because in Swift documentation there's two important concepts that divide the documentation into two parts. The Summary part and the Discussion part.

The summary is what you see just when you are writing the code, the example below show the phrase "The number of the elements in the array" as the summary:

Screenshot 2021-08-30 at 09.06.58.png

In our example this is shown like this:

Screenshot 2021-08-31 at 08.33.00.png

Discussion Part

Now to create the Discussion part you need to add an additional paragraph with one blank line. The Swift markup style will consider everything after the first phrase as discussion part.

    /**
     This is function sum the X value with all the Y values
     and return Optional Int.

     This will be the dicussion section.
     */
    func sumValues(x: Int, y: [Int]) throws -> Int? {

        if x < 0 {
            throw MyError.Number
        }

        guard !y.isEmpty else { return nil }

        return y.reduce(0, +) + x
    }

You should see like this now:

Screenshot 2021-08-31 at 08.40.42.png

Daily Life Markup

You can add a paragraph just adding a blank line in your comment section, another ones you can apply are *italic* and even **bold**:

    /**
     This is function **sum the X value with all the Y values**
     and return Optional Int.

     This will be the *dicussion section*.

     */
    func sumValues(x: Int, y: [Int]) throws -> Int? {

    }

Resulting:

Screenshot 2021-08-31 at 08.58.29.png

Also an important one is the code style, the Optional Int below shows it:

    /**
     This is function **sum the X value with all the Y values**
     and return `Optional Int`.

     This will be the *dicussion section*.

     */
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

Screenshot 2021-08-31 at 09.42.52.png

Headers are made using # signs:

    /**
     This is function **sum the X value with all the Y values**
     and return `Optional<Int>`.

     This will be the *dicussion section*.

     # New Header

     A whole new world!

     */
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

Providing you this pretty new header:

Screenshot 2021-09-01 at 07.26.07.png

Now talking about enumerating things, you can have Lists inside your docs. The lists can have multiple levels and there's two types of lists: ordered and unordered. You can use any of those bullets characters : -, +, *, or •, for unordered lists. For ordered lists you just use: 1. , 2. , 3. and so on... Ordered lists are important if you are doing specific sets of steps that the user should be aware of.

Check the example below:

    /**
     This is function **sum the X value with all the Y values**
     and return `Optional<Int>`.

     This will be the *dicussion section*.

     # So can be ordered lists

     1. You can order
     2. the lists!
     3. For your convenience

     # The unordered lists

     - Apples,
     - Bananas?
     - oranges!!
     - Melons.
       - and watermelons.

     */
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

Now you have great lists:

Screenshot 2021-09-01 at 07.37.20.png

Xcode Special cases

Now you are thinking: Ok but how can I change the parameter description? This and the other cases that we will show from now all share the same pattern. You have a keyword and : , for example: Parameter x:. Check the code below:

    /**
      This is function **sum the X value with all the Y values**
      and return `Optional<Int>`.

      This will be the *dicussion section*.

     - Parameter x: The number of your age.
     - Parameter y: The list of your colleagues ages.
      */
    func sumValues(x: Int, y: [Int]) throws -> Int? {

        if x < 0 {
            throw MyError.Number
        }

        guard !y.isEmpty else { return nil }

        return y.reduce(0, +) + x
    }

Screenshot 2021-09-01 at 07.47.25.png

The same behaviour is applied to documenting throws and returns. You can see them all in action below:

    /**
      This is function **sum the X value with all the Y values**
      and return `Optional<Int>`.

      This will be the *dicussion section*.

     - Parameter x: The number of your age.
     - Parameter y: The list of your colleagues ages.

     - Throws: MyError.Number the *x parameter* should not be less than `zero`

     - Returns: A `Optional Integer` with nil or the sum of your age with your colleagues ages.

      */
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

Documentation for Algorithms

We Swift Developers are very lucky to have a handful keyword to document algorithms. Imagine that you want to document our simple method as a complex algorithm, it could be like this:

    /**
      This is function **sum the X value with all the Y values**
      and return `Optional<Int>`.

      This will be the *dicussion section* for the algortithm.
     - Important: You need to catch the throw error and the user must know about it.
     - Precondition: X is not less than zero and Y can't be empty
     - Postcondition: y is guaranteed not copied and the return value is thread safe
     - Requires: you must run this function in `Runloop.main`
     - Warning: Don't try to rerun the experiment within 5 minutes, it can crash your app.
     - Invariant: There's no such case here.
     - Complexity: *O(n)*
      */
    func sumValues(x: Int, y: [Int]) throws -> Int? {...}

And now you have an extensive explanation about your algorithm:

Screenshot 2021-09-01 at 08.03.03.png

You can also give examples of how your code can be called, and there are two ways to do that. One is adding one tab and two spaces from the beginning of a new line and another is put your code inside two ~~~:

    /**
      This is function **sum the X value with all the Y values**
      and return `Optional<Int>`.

     This will be the *dicussion section* for the algortithm.

     Usage Example 1:
     ~~~
     let pd = PrettyDocs()
     print(try pd.sumValues(x: 9, y: [1,2]))
     ~~~

     Usage Example 2:

          let pd2 = PrettyDocs()
          print(try pd2.sumValues(x: 10, y: [1,2,4,3]))

      */
    func sumValues(x: Int, y: [Int]) throws -> Int? {

        if x < 0 {
            throw MyError.Number
        }

        guard !y.isEmpty else { return nil }
        return y.reduce(0, +) + x
    }

Resulting in those two blocks of code:

Screenshot 2021-09-01 at 08.18.14.png

Horizontal Rules

Insert a horizontal rule by using three or more hyphens (-), asterisks (*), or underscores (_) on a line that is surrounded by empty lines. Each of the characters is the same for a horizontal rule. Whitespace between the characters is ignored.

The code below shows it:

    /**
     This is function **sum the X value with all the Y values**
          and return `Optional<Int>`.

     This is above the horizontal rule, and will be the *dicussion section* for the algortithm.
     * *  * *
     And this is below the horizontal rule :)

     Actually, anything!
          */
    func sumValues(x: Int, y: [Int]) throws -> Int? {
    }

Resulting in the white horizontal rule line here:

Screenshot 2021-09-01 at 10.22.50.png

Fun Docs With Images!

It's seems broken in my Xcode build 12.5 and the new beta also seems to not work but Apple says that is possible to insert images in playgrounds documentations.

It's curious and would be very good to be able to do that but I couldn't get it to work properly. If anyone can make it work, just leave a comment below!

Summary

In this post we study a lot of possibilities around doing documentation. When working in public API or you just want to you code to be more expressive the documentation is a great tool to achieve those goals. A great documentation can save or destroy a project, so be careful using it!

That's all my people, I hope you liked as I enjoyed write this article. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello. You can also sponsor posts! Just reach me in LinkedIn or Twitter for details.

Thanks for the reading and... That's all folks.

Credits: image

Interested in reading more such articles from Leonardo Maia Pugliese?

Support the author by donating an amount of your choice.

 
Share this