1

Suppose I have an object like:

class MyClass
{
   let a_number : Int?
   let a_string : String?
   let an_array_of_strings : Array<String>?
   let an_array_of_objs : Array<Any>?
}

How could I make it so that when I print this object to console, z is indented like so:

MyClass
a_number            = 4
a_string            = "hello"
an_array_of_strings = ["str1",
                      "str2",
                      "str3"]
an_array_of_objs    = [MyClass
                      a_number            = 5
                      a_string            = "world"
                      an_array_of_strings = nil
                      an_array_of_objs    = nil]
GoldenJoe
  • 7,874
  • 7
  • 53
  • 92

1 Answers1

1

I'd do that with a recursive function with an accumulator parameter for the indentation. It defaults to no indentation and is increased by the first column's width on each recursive call:

func describe<T>(_ x: T, indent: String = "") -> String
{
    let mirror = Mirror(reflecting: x)
    guard !mirror.children.isEmpty else { return x is String ?  "\"\(x)\"" : "\(x)" }

    switch mirror.displayStyle! {
    case .tuple:
        let descriptions = mirror.children.map { describe(unwrap($0.value), indent: indent) }
        return "(" + descriptions.joined(separator: ",\n\(indent)") + ")"
    case .collection:
        let descriptions = mirror.children.map { describe(unwrap($0.value), indent: indent) }
        return "[" + descriptions.joined(separator: ",\n\(indent)") + "]"
    case .dictionary:
        let descriptions = mirror.children.map { (child: Mirror.Child) -> String in
            let entryMirrors = Array(Mirror(reflecting: unwrap(child.value)).children)
            return describe(unwrap(entryMirrors[0].value), indent: indent) + ": "
                + describe(unwrap(entryMirrors[1].value))
        }
        return "[" + descriptions.joined(separator: ",\n\(indent)") + "]"
    case .set:
        let descriptions = mirror.children.map { describe(unwrap($0.value), indent: indent) }
        return "Set(" + descriptions.joined(separator: ",\n\(indent)") + ")"
    default:
        let childrenWithLabel = mirror.children.filter { $0.label != nil }
        let separator = " = "
        let firstColumnWidth = (childrenWithLabel.map { Int($0.label!.characters.count) }.max() ?? 0)
            + separator.characters.count
        let subindent = indent + String(repeating: " ", count: firstColumnWidth)

        let lines = childrenWithLabel.map {
            indent
            + ($0.label! + separator).padding(toLength: firstColumnWidth, withPad: " ", startingAt: 0)
            + describe(unwrap($0.value), indent: subindent)
        }
        return (["\(mirror.subjectType)"] + lines).joined(separator: "\n")
    }
}

This function uses the unwrap(_:) function from my answer to another question

func unwrap<T>(_ any: T) -> Any
{
    let mirror = Mirror(reflecting: any)
    guard mirror.displayStyle == .optional, let first = mirror.children.first else {
        return any
    }
    return first.value
}

When using describe(_:) like this (I made MyClass a struct so I can use the memberwise initializer):

struct MyClass: CustomStringConvertible
{
    let a_number : Int?
    let a_string : String?
    let an_array_of_strings : Array<String>?
    let an_array_of_objs : Array<Any>?

    var description: String { return describe(self) }
}

print(MyClass(a_number: 4, a_string: "hello",
              an_array_of_strings: ["str1", "str2", "str3"],
              an_array_of_objs: [
                  MyClass(a_number: 5, a_string: "world",
                          an_array_of_strings: nil, an_array_of_objs: nil)]))

then the output is

MyClass
a_number =            4
a_string =            "hello"
an_array_of_strings = ["str1",
                      "str2",
                      "str3"]
an_array_of_objs =    [MyClass
                      a_number =            5
                      a_string =            "world"
                      an_array_of_strings = nil
                      an_array_of_objs =    nil]

Please note that this is only tested with your specific example and some simple additions. I am also not happy about the forced unwrap of mirror.displayStyle but in my shallow testing this only ever happened when mirror.children is empty, which is covered by the preceding guard. If anybody has investigated this more closely, I'd love a comment. I haven't found anything in the documentation of Mirror.

And just like in my answer to your related question I mixed up where the = is supposed to be. Just the other way round this time, duh! :)

Community
  • 1
  • 1
thm
  • 1,217
  • 10
  • 12
  • Actually I like the way you had the = indented originally so I went ahead and changed it. :) – GoldenJoe Apr 11 '17 at 20:07
  • I was testing this out, and I get some strange results for strings. They print out like: time_held = Any timeIntervalSinceReferenceDate = 513703081.0 – GoldenJoe Apr 12 '17 at 09:19
  • ^ I meant dates. Dates print out weird. My class also prints as "[Any" rather than "[MyClass" – GoldenJoe Apr 12 '17 at 09:27
  • Heh, I hoped that "only tested with your specific example" wouldn't work out so bad. I'll have a look into it (not at the computer right now). Could you in the meantime please add a comment with a snippet how exactly you are calling describe? Other than that, you just added a date property to MyClass? – thm Apr 12 '17 at 09:35
  • @GoldenJoe I could not reproduce the "Any" issue for `Date` or `MyClass` values. Dates do print out as `Date\ntimeIntervalSinceReferenceDate = ...`, just like they do with `po` in lldb (`Date` is a `struct`). If you could give me a code sample, that might help. I also added handling the other display styles (tuple, dictionary, set). – thm Apr 12 '17 at 15:20