Here’s another great write-up from Tim V.
By far the most common way to add a custom textual representation to a type is by implementing CustomStringConvertible
. Consider this type:
struct Person { let name: String let age: Int let spouse: String? let children: [String] }
A pretty standard way to conform to CustomStringConvertible
would be this:
extension Person: CustomStringConvertible { var description: String { var description = "" description += "Name: \(name)\n" description += "Age: \(age)\n" if let spouse = spouse { description += "Spouse: \(spouse)\n" } if !children.isEmpty { description += "Children: \(children.joined(separator: ", "))\n" } return description } }
This code doesn’t look too bad, but it’s a pain to write var description = ""
and return description
over and over, if this is a pattern you commonly use. It’s also quite easy to forget to add \n
to each line.
The relatively unknown standard library protocol TextOutputStreamable
solves both of these problems for you. Rather than adding a description
computed property, all you have to do is write your properties to a TextOutputStream
instance:
extension Person: TextOutputStreamable { func write<Target: TextOutputStream>(to target: inout Target) { print("Name:", name, to: &target) print("Age:", age, to: &target) if let spouse = spouse { print("Spouse:", spouse, to: &target) } if !children.isEmpty { print("Children:", children.joined(separator: ", "), to: &target) } } }
That’s it! Whenever something that conforms to TextOutputStreamable
but not to CustomStringConvertible
is turned into a string, the write(to:)
method we just implemented is used:
let person = Person(name: "Michael", age: 45, spouse: "Emma", children: ["Charlotte", "Jacob"]) print(person) >Name: Michael >Age: 45 >Spouse: Emma >Children: Charlotte, Jacob
If you enjoyed this write-up, you might be interested in an old post I wrote about using streams to transform data.
6 Comments
Great tip!
This may be a silly question, but isn’t
write(to:)
a generic function? If not, won’t we have to declare the type ofTarget
somewhere?Good catch, Chris! It wasn’t visible because it was interpreted as an HTML tag at first, but it’s fixed now.
Awesome, thanks! ✨
How, how are you doing this? I have almost understood everything :(((((
Check out my the talk: https://www.youtube.com/watch?v=kHG_zw75SjE for a more general treatment (1st half of the talk). The code is at https://github.com/mpw/MPWFoundation/tree/master/Streams.subproj Also discussed in the book: iOS and macOS Performance Tuning
The provided solution is not correct, since with multi-threaded output there is a chance that some other lines appear in between Name and Children, so it will be hard to tell when the instance description started and when it ended. There should be only one `print’ function call per instance.