I spend a lot of time working in playgrounds. Playgrounds are fast and focused, allowing me to rapidly prototype Swift code. I use the OS X version more than I ever expected, even when my actual coding destination is iOS. OS X playgrounds run natively and support cross-platform solutions for technologies like SpriteKit, AVFoundation, and Core Image. Importantly, you can add interactive controls like sliders to your OS X playgrounds using nibs, so you can explore outcomes more flexibly, skipping many edit/compile/run tasks.
Simplicity is the key to effective interactive playgrounds. You want your code to focus on just the material you’re developing and no more. If you’re doing more than establishing a few callbacks in your set-up code, you should seriously consider moving large portions of that set-up into your playground’s sources folder.
Xcode 6.3 introduced Resources and Sources folders. “Playgrounds now let you provide auxiliary support source files, which are compiled into a module and automatically imported into your playground. To use the new supporting source files feature, twist open the playground in the project navigator to see the new Sources folder, which has a single file named SupportCode.swift by default. Add code to that file, or create new source files in this folder, which will all be automatically compiled into a module and automatically imported into your playground.”
When restructuring my playgrounds this way, I wanted to provide access to a top-level NSWindow instance and its content view.
/// Playground-accessible global main window public var window : NSWindow! = SelectTypedItemFromArray(type: NSWindow.self, nibItemArray) /// Playground-accessible global content view for main window public var contentView = window.contentView as! NSView
Creating the SelectTypedItemFromArray function was challenging, since it had to select and return an arbitrary type from an NSArray.
/// Fetch item by class public func SelectTypedItemFromArray<T>(type t: T.Type, array: NSArray) -> T? { for eachItem in array { if let typedItem = eachItem as? T { return typedItem } } return nil }
It took a ridiculously long time to figure out how to pass a class to a generic function and return an optional instance of that function even though the final working code is short. This struggle is La Vida Swift in a nutshell: it’s never easy until you figure it out. Use T.Type to set the type parameter, and check each element against that type. You can even let Swift infer the type of the variable for you if you unwrap using !:
public var window = SelectTypedItemFromArray(type: NSWindow.self, nibItemArray)!
Once I figured out the assigned type return pattern, I was able to expand the concept to allow view initialization from items loaded into the nib window’s content view. Swift infers the button’s type as NSButton in the following if-let assignment:
if let button = FetchView(type: NSButton.self) { button.target = delegate; button.action = Selector("react:") }
My view-fetching code ended up looking like this:
/// Fetch view by tag and class from contentView /// Do not re-use tags in the same view hierarchy (Thanks, Psy) public func FetchView<T>(type t: T.Type, tag: Int = 0, contentView: NSView = contentView) -> T? { if tag != 0 { // When the tag is specified, the subview must match that tag if let view : T = contentView.viewWithTag(tag) as? T {return view} } else { // When the tag is 0 or unspecified, find by type using depth-first recursion for eachItem in contentView.subviews { if let view = eachItem as? T {return view} if let subview = FetchView(type: t.self, tag:0, contentView:eachItem as! NSView) { return subview } } } return nil }
Fetching by tag and type takes a lot less work because viewWithTag performs a recursive subview search on your behalf. Always use unique tags for your views. When searching by type without a tag, perform your own recursion (here, it’s depth-first) to retrieve the first matching instance. While you could change this to build a matching class array and return all matching class instances, I mostly need this function to find a single radio group, checkbox, or slider. Adding tags is easy enough without needing to fetch an instance array.
My evolving nib/playground support file can be found here.
Update 1: Alex Pretzlav writes, “Nice trick in your latest blog post. You can actually have your generic functions infer the return type based on assignment to a variable, which I find a little nicer:”
Update 2: A couple of variations on the theme that are less tied to NSArray specifics. (For the second, use NSArray as! [AnyObject])
Update 3: Lily Ballard adds a version that checks more strictly for typing, so a double from [1, 2, “lol”, 5.2, “bar”] returns 5.2 instead of 1.
Update 4: You see the numeric issue with the following arrays. The NSNumber instances used by the first two arrays confuse number typing. The [Any] array mandated by Lily’s version forces Swift evaluation and ensures better type selection.
// NSArray let a = [1, 2, "Hello", 5.2, "There", 2.8, "x"] as NSArray // [NSObject] let b = [1, 2, "Hello", 5.2, "There", 2.8, "x"] // [Any] let c : [Any] = [1, 2, "Hello", 5.2, "There", 2.8, "x"]
That said, this routine is of greatest use when working with UI elements, which all live in the Cocoa and Cocoa Touch world. The select function matches classes and subclasses, so a request to match UILabel, for example, will match UILabel instances and, say, UILabelSubclass instances but not the other way around.
Greg J asks: “Isn’t marking views as of interest by tag is a bit of a hack?”. Answer? Yup. However, without that hack it gets a lot messier since playgrounds don’t properly load nibs with bindings to playground classes. Tagging eliminates view confusion, especially, when working with common view types.
Like the hacks? Read the book.
Comments are closed.