February 13, 2016

Prototyping with XCode Playgrounds

With the introduction of the Swift programming language Apple also introduced Playgrounds, interactive environments for easy sharing & prototyping.

Today I want to share a tiny example on how to use them to prototype custom UIViews. Specifically a PolarClock. You can download the complete Playground here.

I love playgrounds because they allow you to quickly sketch ideas, and visualize steps, before heading for a fully fledged implementation. Let’s try it out:

First, open a recent XCode (I’m using 7.2) and create a new Playground.

We’ll start by constructing a circle using polar coordinates:

let radius = 50.0
var points = [CGPoint]()
for degree in 0.stride(to: 360.0, by: 10) {
  let theta = (90 + degree) * M_PI / 180.0
  let x = radius * cos(theta)
  let y = radius * sin(theta)
  let p = CGPoint(x: -x, y: y)
  points.append(p)
}

While you can not visualize [CGPoint] with Playgrounds, you can visualize a UIBezierPath:

let bezierPath = UIBezierPath()
bezierPath.moveToPoint(points[0])
for var i = 1; i < points.endIndex; i++ {
  bezierPath.addLineToPoint(points[i])
}
bezierPath

When you click the visualization button you should see a circle:

UIBezierPath visualized

Let’s wrap this in a tiny UIView, called Polar:

import UIKit

public class Polar: UIView {
  public var radius: CGFloat = 20
  public var completeness: CGFloat = 0.1 {
    didSet {
      self.setNeedsDisplay()
    }
  }
  public var strokeWidth: CGFloat = 10
  public var stepSize: CGFloat = 0.25
  public var color: UIColor = UIColor.redColor()

  func path(ps: [CGPoint]) -> UIBezierPath {
    let bezierPath = UIBezierPath()
    bezierPath.lineJoinStyle = .Round
    bezierPath.fillWithBlendMode(.Normal, alpha: 0.0)
    bezierPath.moveToPoint(ps[0])
    for var i = 1; i < ps.endIndex; i++ {
      bezierPath.addLineToPoint(ps[i])
    }
    return bezierPath
  }

  func circle(radius: Double) -> [CGPoint] {
    var points = [CGPoint]()
    let center = CGPoint(x: self.frame.width/2, y: self.frame.height/2)

    for degree in 0.0.stride(to: 360.0*Double(self.completeness), by: Double(self.stepSize)) {
      let theta = (270 + degree) * M_PI / 180.0
      let x = radius * cos(theta)
      let y = radius * sin(theta)
      let p = CGPoint(x: Double(center.x)+x, y: Double(center.y)+y)
      points.append(p)
    }

    return points
  }

  public override func drawRect(rect: CGRect) {
    let ctx = UIGraphicsGetCurrentContext()
    CGContextSetLineWidth(ctx, self.strokeWidth)
    CGContextAddPath(ctx, path(circle(Double(self.radius))).CGPath)
    self.color.set()
    CGContextStrokePath(ctx)
  }
}

The Polar view just placed the above code in separate functions and added some properties, as well as a drawRect method to display the view.

The observing reader might spot the changed theta calculation; this is necessary because the coordinates system for UIViews is different than the preview in the playground. To support a clockwise rotation at the center we start at 270°.

Finally, let’s use the class to build a real PolarClock:

let frame = CGRect(x: 0, y: 0, width: 375, height: 667)
let view = UIView(frame: frame)
XCPShowView("PolarClock", view: view)

let colors = [
  UIColor(red: 23.0/255.0, green: 170.0/255.0, blue: 178.0/255.0, alpha: 1.0),
  UIColor(red: 255.0/255.0, green: 32.0/255.0, blue: 172.0/255.0, alpha: 1.0),
  UIColor(red: 7.0/255.0, green: 241.0/255.0, blue: 255.0/255.0, alpha: 1.0)
]

let clockFrame = CGRect(x: 100, y: 100, width: 100, height: 100)
let clock = (
  Polar(frame: clockFrame),
  Polar(frame: clockFrame),
  Polar(frame: clockFrame)
)
clock.0.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
clock.0.color = colors[0]
clock.0.radius = 45.0
view.addSubview(clock.0)

clock.1.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
clock.1.color = colors[1]
clock.1.radius = 35.0
view.addSubview(clock.1)

clock.2.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
clock.2.color = colors[2]
clock.2.radius = 25.0
view.addSubview(clock.2)

let timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue())
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0)
dispatch_source_set_event_handler(timer) {
  let now = NSDate()
  let calendar = NSCalendar.currentCalendar()
  let components = calendar.components([NSCalendarUnit.Hour, NSCalendarUnit.Minute, NSCalendarUnit.Second], fromDate: now)
  let hour = components.hour
  let minutes = components.minute
  let seconds = components.second

  clock.0.completeness = CGFloat(hour) / 24.0
  clock.1.completeness = CGFloat(minutes) / 60.0
  clock.2.completeness = CGFloat(seconds) / 60.0
}
dispatch_resume(timer)

You’ll see something like this, before the playground terminates after some seconds:

UIBezierPath visualized

That’s it! Now it’s just a tiny step to implement the @IBDesignable and use the class inside interface builder:

@IBDesignable
public class Polar: UIView {
  @IBInspectable public var radius: CGFloat = 20
  @IBInspectable public var completeness: CGFloat = 0.1
  @IBInspectable public var strokeWidth: CGFloat = 10
  @IBInspectable public var stepSize: CGFloat = 0.25
  @IBInspectable public var color: UIColor = UIColor.redColor()
  // … omitted
}

Now you can also use your Polar view inside interface builder:

UIBezierPath visualized

I love that you can easily prototype complete views with Playgrounds. The only catch is that only Swift is supported right now, so hopefully you can live without a debugger.

All in all the XCode playgrounds are awesome to share executable code, teach and learn. You should use them probably, too.

Anyway, that’s it for today. Happy hacking!

© Raphael Randschau 2010 - 2022 | Impressum