Video #21: Playground Driven Development
Episode: Video #21 Date: Jul 9, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep21-playground-driven-development

Description
We use Swift playgrounds on this series as a tool to dive deep into functional programming concepts, but they can be so much more. Today we demonstrate a few tricks to allow you to use playgrounds for everyday development, allowing for a faster iteration cycle.
Video
Cloudflare Stream video ID: 01d2f0b3df0be03040f8cd6d9c8d466e Local file: video_21_playground-driven-development.mp4 *(download with --video 21)*
References
- Discussions
- Episode Code Samples repo
- Overture
- Playground Driven Development
- Playground Driven Development at Kickstarter
- 0021-playground-driven-development
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
Today’s episode is going to be a bit different. We’re going to take a peek behind the Point-Free curtain to look at how we write code every day. Our episodes focus on experimenting in playgrounds, and in a buzzword-y kind of way you could consider this “playground-driven development”. This is how we write most of our code, including for this site and iOS applications we’ve worked on. This style of development has a lot of benefits, but it’s not always clear how to get started and incorporate playgrounds into an application and its development cycle.
— 0:42
We’re going to show our workflow and, with an example, show how folks can apply this technique to their own code bases. Exploring an example playground
— 1:01
We’ll start with a fresh checkout of our Episode Code Samples repo . We’re going to take the playground from our most recent UIKit-based episode and incorporate it into an iOS application. $ cd 0017-styling-pt2 $ ls README.md Styling-2.playground Vendor ep0017.xcworkspace
— 1:27
In this directory we have our playground, a Vendor directory housing our Overture library, and a workspace that exposes Overture to our playground. $ ls Vendor swift-overture
— 1:51
Let’s explore our workspace. We can open it right from the command line using xed , a tool Apple provides as a means of editing files using Xcode. $ xed .
— 2:07
The workspace opens up to the playground we worked on last time. When we run the playground, the assistant editor displays a live view of the playground’s view controller, a list view of Point-Free episodes. This gave us a very tight feedback loop when we were refactoring, giving us an instant view of our changes and allowing us to more quickly correct any mistakes along the way.
— 2:31
We record our episodes with Xcode’s toolbar and navigator collapsed to focus on the playground content, but this time we’re going to take a look at what might be hiding there. We can now see the Overture project file alongside the playground, which allows us to both build Overture in this workspace, and import it into our playground. Our playground’s Sources directory also has a few helper files in it.
— 2:57
Let’s extract this playground into an application. We’ll start by creating a new Xcode project using the Single View App template for iOS.
— 3:10
Because we have our workspace open, we’re given the option to add it to our workspace when we create it. And with this project added to our workspace we’re immediately able to build its iOS app.
— 3:34
Let’s first drag our playground’s source files over to the application group, make sure to copy them, and delete the originals.
— 3:49
And then the last step is to copy the playground code over into the app! We’ll just reuse the ViewController.swift stub that Apple generates for now.
— 4:07
When we try to build again we hit our first snag. No such module ‘Overture’ Our application does not yet depend on Overture.
— 4:16
We can fix this by going to the target settings of our application and adding Overture to its Embedded Binaries.
— 4:22
Now the build succeeds.
— 4:28
Let’s do a little bit of project cleanup. We’re programmatically laying out our view controller and not using storyboards, so let’s get clear the User interface target setting and delete the main storyboard.
— 4:42
Without a storyboard entry point, we need to modify our app delegate to explicitly set up our application. We can change the optional UIWindow? property to be a non-optional, instantiated version of itself and then configure it in application(didFinishLaunchingWithOptions:) . @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { let window = UIWindow() func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { self.window.rootViewController = EpisodeListViewController() self.window.makeKeyAndVisible() return true } Correction This code is wrong, unfortunately. While the application will build and the window will be visible, we’ve broken the optional protocol requirement on UIApplicationDelegate , which is: var window: UIWindow? { get set } Because of this, window will always be nil on our delegate when accessed through the UIApplicationDelegate protocol. UIApplication.shared.delegate?.window // nil The fix is to go back to explicitly using an optional, mutable variable: var window: UIWindow? = UIWindow()
— 5:08
Now, the project builds, runs, and displays our episode list view! App frameworks
— 5:19
That’s all it takes to bring an isolated playground into an application, but unfortunately we lost something in the move: the ability to access this code from a playground. This is our first caveat of playground-driven development: anything in an app target is not visible to a playground because app targets don’t define a module that’s accessible to playgrounds.
— 5:50
There’s an easy fix, though! We can create a new framework, put all of our app code in there, and then our playgrounds will have access to it.
— 6:07
First, we’ll go to the app’s build settings, add a new target, select the Cocoa Touch Framework template, which we’ll call PointFreeFramework . Xcode automatically embedded it in our app target.
— 6:28
Next, we select the files we created earlier and move them over to the framework group, which will update them to be members of the framework target.
— 6:45
When we try to build, we get an error because our app target can no longer see the view controller that got moved to our framework. Use of unresolved identifier ‘EpisodeListViewController’ First, we need to import our framework. import PointFreeFramework After that, we need to make our view controller public. public final class EpisodeListViewController: UITableViewController {
— 7:06
And with those small changes, it builds and runs, just as before! The big difference is now we can go into a playground, import our framework, and get access to our view controller again. import PointFreeFramework import PlaygroundSupport let vc = EpisodeListViewController() PlaygroundPage.current.liveView = vc
— 7:45
And we can see everything rendered in the live view again. Now we have one single framework that’s holding all of our styling, utilities, models and view controllers, and we have access to them all in both our app target and our playgrounds.
— 8:04
An interesting thing to note is that our app target includes just one file: our app delegate. When you develop in this way, the app target is merely a host for your main framework. You use the app delegate to get access to that root view controller, stick it in the window, and that’s all the app target is responsible for. Everything else is handed off to the app framework.
— 8:28
Now there is a subtle issue we need to deal with, which isn’t apparent right now because of how Xcode build artifacts are cached. The files in our framework depend on Overture, but Overture isn’t a dependency of our framework. We may not have even noticed this issue for awhile, but if we clean the build folder, we’ll see the build error. No such module ‘Overture’
— 8:57
What we need to do is link Overture to PointFreeFramework, and everything should build just fine. This is just a small thing to keep in mind when you’re adding and moving around these files and frameworks in your project. Playground-driven development
— 9:23
This is great! We went from a world that existed purely in playgrounds, to a world where our code could be built and run in a simulator or on device but could not be loaded in a playground, finally to a world where we have the best of both worlds.
— 9:34
What’s it look like to develop in an app structured this way? Let’s add a feature to our episode list view. On our website, we indicate whether an episode is subscriber-only or not. It might be nice to get some feature parity in the iOS app by adding some indication in the poster view.
— 10:04
We can start by opening the view controller code up in a new tab, and navigate to the EpisodeCell . Let’s add a subscriber-only label. private let subscriberOnlyLabel = UILabel()
— 10:23
This label is going to float over our poster image, so we want to add it to our view hierarchy at the very end. self.contentView.addSubview(self.subscriberOnlyLabel)
— 10:32
We also want to configure it. self.subscriberOnlyLabel.text = "Subscriber Only" And add a couple constraints to pin it to the top-right of the cell. NSLayoutConstraint.activate([ … self.subscriberOnlyLabel.topAnchor.constraint( equalTo: self.posterImageView.topAnchor, constant: .pf_grid(3) ), self.subscriberOnlyLabel.trailingAnchor.constraint( equalTo: self.posterImageView.trailingAnchor, constant: .pf_grid(6) ), ])
— 10:57
We’ve made a very small change, and to see it in action, we can change our build target to the app framework, build, and hop back over to our playground…
— 11:11
What happened to our cells? We forgot to configure our label for auto-layout. Luckily, we were able to see the issue very quickly without having to build the entire application, run it, and do any navigation to get to that screen. Let’s fix that. with(self.subscriberOnlyLabel, autoLayoutStyle) A quick build and our playground is rendering everything again. And one more thing to quickly fix: the trailing constant needs to be negative. NSLayoutConstraint.activate([ … self.subscriberOnlyLabel.trailingAnchor.constraint( equalTo: self.posterImageView.trailingAnchor, constant: -.pf_grid(6) ),
— 11:56
One more build, one more peek at our playground, and things are starting to look good! We were able to very quickly iterate on this feature and address problems along the way. Because our view controller lives in the framework now, we do have to do a little hopping back and forth between its code and the playground, but the framework builds very quickly and we have a feedback loop that’s way faster than building the whole application, waiting for the simulator, navigating to the screen, etc.
— 12:24
It’s also a bit nicer than storyboards, since storyboards don’t give us a true representation of how a screen is going to render. We’re even pulling in real data and showing it, which storyboards can’t do.
— 12:38
One thing to note is we’re using a key-combo shortcut that doesn’t ship with Xcode 9, including one that runs the playground. By default, Xcode runs playgrounds automatically whenever they change, but this can cause problems over time, where playgrounds become less and less reliable and responsive, usually requiring a restart. We’ve found that Xcode playground stability goes way up by switching things to run manually, instead.
— 13:03
When it’s set to Manually Run, you may find yourself having to manually press the “play” button to re-run a playground, which can be a little inconvenient. In order to improve this experience, we’ve gone to the Key Bindings section of Xcode’s preferences, and added a binding to Execute Playground.
— 13:37
Alright, now that we have a feel for iteration, let’s tweak how the label looks.
— 13:50
Let’s make the label white and reuse the caps font style that appears below the poster. First, we can extract the font style from the sequenceAndDateLabel . let smallCapsLabelStyle = mut( \UILabel.font, UIFont.preferredFont(forTextStyle: .caption1).smallCaps ) with(self.sequenceAndDateLabel, smallCapsLabelStyle)
— 14:32
And we can reuse this style for the subscriberOnlyLabel . with(self.subscriberOnlyLabel, concat( autoLayoutStyle, smallCapsLabelStyle, mut(\.textColor, .white) ))
— 14:48
We’ll probably be reusing this style even more in the future, so let’s extract it to where our other styles are defined. Why are these styles still defined here, though? Maybe it’s time to extract them to a new file, which we can call StyleGuide.swift.
— 15:20
Everything still builds and our view controller file got a lot shorter.
— 15:25
Let’s hop back over to the playground to see how everything renders. As it stands it’s pretty hard to read, especially on lighter backgrounds. Let’s add a background color to make things easier to read. with(self.subscriberOnlyLabel, concat( autoLayoutStyle, smallCapsLabelStyle, mut(\.textColor, .white), mut(\.backgroundColor, UIColor(white: 0, alpha: 0.3)) ))
— 15:55
It looks better and we can read it, but it might look even nicer if we added some padding around it, and maybe round the corners. UIKit doesn’t provide a way to add padding to a label, or in general, but in our episode on Contravariance , we defined a wrapView function to demonstrate contravariance, but it’s the perfect tool we need here. func wrapView(padding: UIEdgeInsets) -> (UIView) -> UIView { return { subview in let wrapper = UIView() subview.translatesAutoresizingMaskIntoConstraints = false wrapper.addSubview(subview) NSLayoutConstraint.activate([ subview.leadingAnchor.constraint( equalTo: wrapper.leadingAnchor, constant: padding.left ), subview.rightAnchor.constraint( equalTo: wrapper.rightAnchor, constant: -padding.right ), subview.topAnchor.constraint( equalTo: wrapper.topAnchor, constant: padding.top ), subview.bottomAnchor.constraint( equalTo: wrapper.bottomAnchor, constant: -padding.bottom ), ]) return wrapper } }
— 17:00
Let’s copy this over to our Utils file, and start using it. let subscriberOnlyLabelWrapper = wrapView( padding: UIEdgeInsets( top: .pf_grid(1), left: .pf_grid(2), bottom: .pf_grid(1), right: .pf_grid(2) ) )(self.subscriberOnlyLabel) with(self.subscriberOnlyLabel, concat( autoLayoutStyle, smallCapsLabelStyle, mut(\.textColor, .white), mut(\.backgroundColor, UIColor(white: 0, alpha: 0.3)) )) And this is what we’ll add as a subview and pin to the corner. self.contentView.addSubview(subscriberOnlyLabelWrapper) NSLayoutConstraint.activate([ … subscriberOnlyLabelWrapper.topAnchor.constraint( equalTo: self.posterImageView.topAnchor, constant: .pf_grid(3) ), subscriberOnlyLabelWrapper.trailingAnchor.constraint( equalTo: self.posterImageView.trailingAnchor, constant: -.pf_grid(6) ), ])
— 17:48
We need to style this wrapper, and we can do so inline using Overture. let subscriberOnlyLabelWrapper = with( wrapView( padding: UIEdgeInsets( top: .pf_grid(1), left: .pf_grid(2), bottom: .pf_grid(1), right: .pf_grid(2) ) )(self.subscriberOnlyLabel), concat( autoLayoutStyle, mut(\UIView.backgroundColor, UIColor(white: 0, alpha: 0.3)) ) ) with(self.subscriberOnlyLabel, concat( smallCapsLabelStyle, mut(\.textColor, .white) ))
— 18:41
Checking things out in the playground, it looks a lot better! The padding gives us some much-needed breathing room. What about those rounded corners?
— 18:49
We just need to reuse our baseRoundedStyle function. let subscriberOnlyLabelWrapper = with( wrapView( padding: UIEdgeInsets( top: .pf_grid(1), left: .pf_grid(2), bottom: .pf_grid(1), right: .pf_grid(2) ) )(self.subscriberOnlyLabel), concat( autoLayoutStyle, baseRoundedStyle, mut(\UIView.backgroundColor, UIColor(white: 0, alpha: 0.3)) ) ) Looking good! But right now it’s showing up on every cell, not just the subscriber-only ones. Let’s fix that. Another feature
— 19:07
Our cell defines a configure(with:) function that configures a cell given an Episode . Episodes have a subscriberOnly property that we can use to show/hide our brand new label. self.subscriberOnlyLabel.isHidden = !episode.subscriberOnly Looking in the playground and this is not what we wanted. We’re hiding the label but the rounded wrapper persists! What we really want is to extract the wrapper to be a property, as well.
— 19:59
We can cut and paste our definition outside of our initializer, and we can use lazy var so that we can reference self.subscriberOnlyLabel in the expression. private lazy var subscriberOnlyLabelWrapper = with( wrapView( padding: UIEdgeInsets( top: .pf_grid(1), left: .pf_grid(2), bottom: .pf_grid(1), right: .pf_grid(2) ) )(self.subscriberOnlyLabel), concat( autoLayoutStyle, baseRoundedStyle, mut(\UIView.backgroundColor, UIColor(white: 0, alpha: 0.3 ) )
— 20:15
It builds, which means we just need to use this view in our earlier logic. self.subscriberOnlyLabelWrapper.isHidden = !episode.subscriberOnly And back in the playground everything’s rendering as we’d expect.
— 20:33
It’s easy to take this kind of feedback loop for granted at Point-Free, as we’re so used to it! We made a ton of tiny changes to our UI and logic and were able to see the effects almost immediately. And our app gets all of these updates automatically! We can build to the simulator or device and confirm the changes there, but were able to do all of the earlier work simply using our framework and playground.
— 21:22
It’s worth mentioning a couple caveats. Unfortunately, not every code base is going to be able to implement this style immediately. There’s a bit of upfront work to getting there. Everything needs to be extracted to a framework, which may not be easy to do. For larger applications this might take awhile! More difficult is ensuring that your application’s view controllers can be instantiated and run in isolation. Loading a playground should ideally not execute a network request or tracking event, so your app’s dependencies are ideally controlled. Luckily, we’ve talked about dependencies before and will have more to say in the future. This is why it’s so important to care about dependencies, though, since it empowers us to adopt playground-driven development and explore our app’s screens this way. We also don’t want a ton of layers of abstraction between creating and rendering view controllers. It should be easy to instantiate them and assign them to the playground’s liveView . Playground-driven development isn’t the only benefit of adopting these patterns: your app code will benefit in general. To be able to load a view controller in isolation means that it’s well set up for tests! Running a view controller in a playground isn’t much different than running a view controller in a test. The work put into getting a project set up for playgrounds is great work to do.
— 23:25
Making view controllers easy to test means making them easy to snapshot test, and moving things to a framework means faster builds and faster tests, since there’s no need to run tests in a simulator using a host app. Running tests in a host app can cause some problems when app code runs every time. Running tests against a framework avoids these problems. What’s the point?
— 24:08
Another episode where we don’t really need to ask this question! We just wanted to show everyone a real-world, applicable, fantastic technique that we use all the time and hope others can adopt, as well. References Playground Driven Development Brandon Williams • Oct 6, 2017 Brandon gave an in-depth talk on playground driven development at FrenchKit 2017. In this talk he shows what it takes to get a codebase into shape for this style of development, and shows off some of the amazing things you can do once you have it. https://www.youtube.com/watch?v=DrdxSNG-_DE Playground Driven Development at Kickstarter Brandon Williams • May 19, 2017 We pioneered playground driven development while we were at Kickstarter, where we replaced the majority of our use for storyboards with playgrounds. It takes a little bit of work to get started, but once you do it really pays dividends. In this Swift Talk episode, Brandon sits down with Chris Eidhof to show the ins and outs of playground driven development. https://talk.objc.io/episodes/S01E51-playground-driven-development-at-kickstarter Downloads Sample code 0021-playground-driven-development Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .