Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
553 lines (492 sloc) 17.2 KB
title author category excerpt status
CustomPlaygroundDisplayConvertible
Mattt
Swift
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.
swift
4.2

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:

{::nomarkdown }

Category Types Result
{% asset playground-icon-string.svg width=24 height=24 @inline %} Strings
  • String
  • NSString
"Hello, world!"
{% asset playground-icon-attributed-string.svg width=24 height=24 @inline %} Attributed Strings
  • NSAttributedString
"Hello, world!"
{% asset playground-icon-number.svg width=24 height=24 @inline %} Numbers
  • Int, UInt, …
  • Double, Float, …
  • CGFloat
  • NSNumber
42
{% asset playground-icon-range.svg width=24 height=24 @inline %} Ranges
  • NSRange
{0, 10}
{% asset playground-icon-boolean.svg width=24 height=24 @inline %} Boolean Values
  • Bool
true
{% asset playground-icon-pointer.svg width=24 height=24 @inline %} Pointers
  • UnsafePointer
  • UnsafeMutablePointer
  • UnsafeRawPointer
  • UnsafeMutableRawPointer
0x0123456789ABCDEF
{% asset playground-icon-date.svg width=24 height=24 @inline %} Dates
  • Date
  • NSDate
Nov 12, 2018 at 10:00
{% asset playground-icon-url.svg width=24 height=24 @inline %} URLs
  • URL
  • NSURL
https://nshipster.com
{% asset playground-icon-color.svg width=24 height=24 @inline %} Colors
  • CGColor
  • NSColor
  • UIColor
  • CIColor
🔴 r 1.0 g 0.0 b 0.0 a 1.0
{% asset playground-custom-description-color.png %}
{% asset playground-icon-geometry.svg width=24 height=24 @inline %} Geometry
  • CGPoint
  • CGSize
  • CGRect
{x 0 y 0 w 100 h 100}
{% asset playground-custom-description-geometry.png %}
{% asset playground-icon-bezier-path.svg width=24 height=24 @inline %} Bezier Paths
  • NSBezierPath
  • UIBezierPath
11 path elements
{% asset playground-icon-image.svg width=24 height=24 @inline %} Images
  • CGImage
  • NSCursor
  • NSBitmapImageRep
  • NSImage
  • UIImage
w 50 h 50
{% asset playground-icon-sprite-kit.svg width=24 height=24 @inline %} SpriteKit Nodes
  • SKShapeNode
  • SKSpriteNode
  • SKTexture
  • SKTextureAtlas
SKShapeNode
{% asset playground-icon-sprite-kit.svg width=50 height=50 @inline %}
{% asset playground-icon-view.svg width=24 height=24 @inline %} Views
  • NSView
  • UIView
NSView
{:/}

{% info %} This list is derived from the source code for the CustomOpaqueLoggable protocol, and is subject to change in future releases. {% endinfo %}

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: "johnny@apple.com")
]

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

``

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:

{% asset playground-custom-description-contact.png %}

{% error %} At the time of writing, the initialization pattern for Playground log entries causes the custom description / debug description of the original result to be discarded. As far as we can tell, there's currently no way to provide a specialized Quick Look representation that doesn't override the result sidebar representation to the normalized type name of the value. {% enderror %}


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.

{% info %} For more information, check out our article about Quick Look debugging. {% endinfo %}

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.