Custom​Playground​Display​Convertible

Playgrounds allow you to see what your Swift code is doing every step along the way. Each time a statement is executed, its result is logged to the sidebar along the right-hand side. From there, you can open a Quick Look preview of the result in a popover or display the result inline, directly in the code editor.

The code responsible for providing this feedback is provided by the PlaygroundLogger framework, which is part of the open source swift-xcode-playground-support project.

Reading through the code, we learn that the Playground logger distinguishes between structured values, whose state is disclosed by inspecting its internal members, and opaque values, which provide a specialized representation of itself. Beyond those two, the logger recognizes entry and exit points for scopes (control flow statements, functions, et cetera) as well as runtime errors (caused by implicitly unwrapping nil values, fatalError(), and the like) Anything else — imports, assignments, blank lines — are considered gaps

Built-In Opaque Representations

The Playground logger provides built-in opaque representations for many of the types you’re likely to interact with in Foundation, UIKit, AppKit, SpriteKit, CoreGraphics, CoreImage, and the Swift standard library:

Category Types Result
Strings
  • String
  • NSString
"Hello, world!"
Attributed Strings
  • NSAttributedString
"Hello, world!"
Numbers
  • Int, UInt, …
  • Double, Float, …
  • CGFloat
  • NSNumber
42
Ranges
  • NSRange
{0, 10}
Boolean Values
  • Bool
true
Pointers
  • UnsafePointer
  • UnsafeMutablePointer
  • UnsafeRawPointer
  • UnsafeMutableRawPointer
0x0123456789ABCDEF
Dates
  • Date
  • NSDate
Nov 12, 2018 at 10:00
URLs
  • URL
  • NSURL
https://nshipster.com
Colors
  • CGColor
  • NSColor
  • UIColor
  • CIColor
🔴 r 1.0 g 0.0 b 0.0 a 1.0
Geometry
  • CGPoint
  • CGSize
  • CGRect
{x 0 y 0 w 100 h 100}
Bezier Paths
  • NSBezierPath
  • UIBezierPath
11 path elements
Images
  • CGImage
  • NSCursor
  • NSBitmapImageRep
  • NSImage
  • UIImage
w 50 h 50
SpriteKit Nodes
  • SKShapeNode
  • SKSpriteNode
  • SKTexture
  • SKTextureAtlas
SKShapeNode
Views
  • NSView
  • UIView
NSView

Structured Values

Alternatively, the Playground logger provides for values to be represented structurally — without requiring an implementation of the CustomReflectable protocol.

This works if the value is a tuple, an enumeration case, or an instance of a class or structure. It handles aggregates, or values bridged from an Objective-C class, as well as containers, like arrays and dictionaries. If the value is an optional, the logger will implicitly unwrap its value, if present.

Customizing How Results Are Logged In Playgrounds

Developers can customize how the Playground logger displays results by extending types to adopt the CustomPlaygroundDisplayConvertible protocol and implement the required playgroundDescription computed property.

For example, let’s say you’re using Playgrounds to familiarize yourself with the Contacts framework. (Note: the Contacts framework is unavailable in Swift Playgrounds for iPad) You create a new CNMutableContact, set the givenName and familyName properties, and provide an array of CNLabeledValue values to the emailAddresses property:

import Contacts

let contact = CNMutableContact()
contact.givenName = "Johnny"
contact.familyName = "Appleseed"
contact.emailAddresses = [
    CNLabeledValue(label: CNLabelWork,
                   value: "[email protected]")
]

If you were hoping for feedback to validate your API usage, you’d be disappointed by what shows up in the results sidebar:

`<CNMutableContact: 0x7ff727e38bb0: ... />`

To improve on this, we can extend the superclass of CNMutableContact, CNContact, and have it conform to CustomPlaygroundDisplayConvertible. The Contacts framework includes CNContactFormatter, which offers a convenient way to summarize a contact:

extension CNContact: CustomPlaygroundDisplayConvertible {
    public var playgroundDescription: Any {
        return CNContactFormatter.string(from: self, style: .fullName) ?? ""
    }
}

By putting this at the top of our Playground (or in a separate file in the Playground’s auxilliary sources), our contact from before now provides a much nicer Quick Look representation:

"Johnny Appleseed"

To provide a specialized Playground representation, delegate to one of the value types listed in the table above. In this case, the ContactsUI framework provides a CNContactViewController class whose view property we can use here (annoyingly, the API is slightly different between iOS and macOS, hence the compiler directives):

import Contacts
import ContactsUI

extension CNContact: CustomPlaygroundDisplayConvertible {
    public var playgroundDescription: Any {
        let viewController: CNContactViewController
        #if os(macOS)
            viewController = CNContactViewController()
            viewController.contact = self
        #elseif os(iOS)
            viewController = CNContactViewController(for: self)
        #else
            #warning("ContactsUI unavailable")
        #endif

        return viewController.view
    }
}

After replacing our original playgroundDescription implementation, our contact displays with the following UI:


Playgrounds occupy an interesting space in the Xcode tooling ecosystem. It’s neither a primary debugging interface, nor a mechanism for communicating with the end user. Rather, it draws upon both low-level and user-facing functionality to provide a richer development experience. Because of this, it can be difficult to understand how Playgrounds fit in with everything else.

Here’s a run-down of some related functionality:

Relationship to CustomStringConvertible and CustomDebugStringConvertible

The Playground logger uses the following criteria when determining how to represent a value in the results sidebar:

  • If the value is a String, return that value
  • If the value is CustomStringConvertible or CustomDebugStringConvertible, return String(reflecting:)
  • If the value is an enumeration (as determined by Mirror), return String(describing:)
  • Otherwise, return the type name, normalizing to remove the module from the fully-qualified name

Therefore, you can customize the Playground description for types by providing conformance to CustomStringConvertible or CustomDebugStringConvertible.

So the question becomes, “How do I decide which of these protocols to adopt?”

Here are some general guidelines:

  • Use CustomStringConvertible (description) to represent values in a way that’s appropriate for users.
  • Use CustomDebugStringConvertible (debugDescription) to represent values in a way that’s appropriate for developers.
  • Use CustomPlaygroundDisplayConvertible (playgroundDescription) to represent values in a way that’s appropriate for developers in the context of a Playground.

Within a Playground, expressiveness is prioritized over raw execution. So we have some leeway on how much work is required to generate descriptions.

For example, the default representation of most sequences is the type name (often with cryptic generic constraints):

let evens = sequence(first: 0, next: {$0 + 2})

UnfoldSequence<Int, (Optional, Bool)>

Iterating a sequence has unknown performance characteristics, so it would be inappropriate to include that within a description or debugDescription. But in a Playground? Sure, go nuts — by associating it in the Playground itself, there’s little risk in that code making it into production.

So back to our original example, let’s see how CustomPlaygroundDisplayConvertible can help us decipher our sequence:

extension UnfoldSequence: CustomPlaygroundDisplayConvertible
           where Element: CustomStringConvertible
{
    public var playgroundDescription: Any {
        return prefix(10).map{$0.description}
            .joined(separator: ", ") + "…"
    }
}

0, 2, 4, 6, 8, 10, 12, 14, 16, 18…

Relationship to Debug Quick Look

When a Playground logs structured values, it provides an interface similar to what you find when running an Xcode project in debug mode.

What this means in practice is that Playgrounds can approximate a debugger interface when working with structured types.

For example, the description of a Data value doesn’t tell us much:

let data = "Hello, world!".data(using: .utf8)

coming from description

13 bytes

And for good reason! As we described in the previous section, we want to keep the implementation of description nice and snappy.

By contrast, the structured representation of the same data object when viewed from a Playground tells us the size and memory address — it even shows an inline byte array for up to length 64:

count 13 pointer "UnsafePointer(7FFCFB609470)" [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]


Playgrounds use a combination of language features and tooling to provide a real-time, interactive development environment. With the CustomPlaygroundDisplayConvertible protocol, you can leverage this introspection for your own types.

NSMutableHipster

Questions? Corrections? Issues and pull requests are always welcome.

This article uses Swift version 4.2. Find status information for all articles on the status page.

Written by Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon.

Next Article

VSCode is a cross-platform text and source code editor from Microsoft, and among the first tools to support Language Server Protocol. With LSP for Swift now shipping in Xcode, it’s a great time to see how this integration works for yourself.