CustomPlaygroundDisplayConvertible
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, fatal
, 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 |
|
"Hello, world!" | |
Attributed Strings |
|
"Hello, world!" | |
Numbers |
|
42 | |
Ranges |
|
{0, 10} | |
Boolean Values |
|
true | |
Pointers |
|
0x0123456789ABCDEF | |
Dates |
|
Nov 12, 2018 at 10:00 | |
URLs |
|
https://nshipster.com | |
Colors |
|
🔴 r 1.0 g 0.0 b 0.0 a 1.0
|
|
Geometry |
|
{x 0 y 0 w 100 h 100}
|
|
Bezier Paths |
|
11 path elements | |
Images |
|
w 50 h 50 | |
SpriteKit Nodes |
|
SKShapeNode
|
|
Views |
|
NSView |
Structured Values
Alternatively,
the Playground logger provides for values to be represented structurally —
without requiring an implementation of the
Custom
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 Custom
protocol
and implement the required playground
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 CNMutable
,
set the given
and family
properties,
and provide an array of CNLabeled
values
to the email
property:
import Contacts
let contact = CNMutable Contact()
contact.given Name = "Johnny"
contact.family Name = "Appleseed"
contact.email Addresses = [
CNLabeled Value(label: CNLabel Work,
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 CNMutable
,
CNContact
,
and have it conform to Custom
.
The Contacts framework includes CNContact
,
which offers a convenient way to summarize a contact:
extension CNContact: Custom Playground Display Convertible {
public var playground Description: Any {
return CNContact Formatter.string(from: self, style: .full Name) ?? ""
}
}
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 CNContact
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 Contacts UI
extension CNContact: Custom Playground Display Convertible {
public var playground Description: Any {
let view Controller: CNContact View Controller
#if os(mac OS)
view Controller = CNContact View Controller()
view Controller.contact = self
#elseif os(i OS)
view Controller = CNContact View Controller(for: self)
#else
#warning("Contacts UI unavailable")
#endif
return view Controller.view
}
}
After replacing our original playground
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
Custom
orString Convertible Custom
, returnDebug String Convertible String(reflecting:)
- If the value is an enumeration
(as determined by
Mirror
), returnString(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
Custom
or Custom
.
So the question becomes, “How do I decide which of these protocols to adopt?”
Here are some general guidelines:
- Use
Custom
(String Convertible description
) to represent values in a way that’s appropriate for users. - Use
Custom
(Debug String Convertible debug
) to represent values in a way that’s appropriate for developers.Description - Use
Custom
(Playground Display Convertible playground
) to represent values in a way that’s appropriate for developers in the context of a Playground.Description
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
Iterating a sequence has unknown performance characteristics,
so it would be inappropriate to include that
within a description
or debug
.
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 Custom
can help us decipher our sequence:
extension Unfold Sequence: Custom Playground Display Convertible
where Element: Custom String Convertible
{
public var playground Description: 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 Custom
protocol,
you can leverage this introspection for your own types.