Computed Properties & Properties with a Closure

Ken Boucher
By Ken Boucher under Engineering 23 February 2016

TABTips

In an effort to share cool things we've learned at TAB, welcome to the first in what we hope will be many of #TABTips! This will be a series where we share cool things we've learnt on projects and in our spare time.

Wondering...

On a current project, I had been wondering what the difference is between the 2 syntactically similar declarations of a property.

// Property declared with a closure
var gradientLayer: CAGradientLayer = {
    let gradientLayer = CAGradientLayer()
    return gradientLayer
}()
// Computed property
var gradientLayer: CAGradientLayer {
	let gradientLayer = CAGradientLayer()
	return gradientLayer
}

A property defined with a closure is defined once and stored to that variable. A computed property acts like a function, and in the example above will return a new CAGradientLayer object.

This gives us a great deal of flexibility with properties, allowing us to define a property once, or essentially create a shorthand for a function. Properties with closures are great to tidy up initialisation calls too, reducing the boiler plate code often found in initialisers.

An example

To provide an example of when we may want to use both, I want to create a new CAGradientLayer object that is set to the bounds of the view. I then want to set the colors property to a predefined set of colours defined by the class, but I dont' want to recreate this array every time since it won't change in our case.

class ExampleGradientView: UIView {

	var newGradientLayer: CAGradientLayer {
		let gradientLayer = CAGradientLayer()
		gradientLayer.bounds = bounds
		return gradientLayer
	}
	
	var gradientColors: [CGColor] = {
	    return [
	      UIColor.blackColor().CGColor,
	      UIColor.whiteColor().CGColor
		]
	}()
}

Note: Renamed the computed property to newGradientLayer so that it is clear that we are always going to return a new CAGradientLayer object.

Our var gradientColors will only be defined once now, but gradientLayer will be defined and fit to the bounds of the view as expected.

Computed Properties for Sizing

I've also been experimenting with using computed properties to define the sizing of cells for flow layouts in collection views. This allows for my code to be incredibly succinct, and for the cell sizes to be defined dynamically.

class ExampleViewController: UIViewController, UICollectionViewDelegate {
	
	var items: [ExampleItem] = []

	// Spacing
	let sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
	let cellSpacing: Double = 10
	
	// Cell sizing
	
	let cellHeight: Double = 104
	var cellWidth: Double {
		switch items.count {
		case 1:
			return Double(CGRectGetWidth(collectionView.bounds) - sectionInset.left - sectionInset.right)
		case 2:
			return (Double(CGRectGetWidth(collectionView.bounds) - sectionInset.left - sectionInset.right) - cellSpacing) / 2.0
		default:
			return 136
		}
	}
  
	function collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
	{
		return CGSize(width: cellWidth, height: cellHeight)
	}
}

In the above example, we have an items property, an array of ExampleItem. We have 3 potential layouts for the cells:

We use a computed property to define the width of the cell dynamically based on the number of items in our items array. The computed property is essentially a function, but provides a neater shorthand for us to call it, by just calling cellWidth.

Closing

My advice with computed properties is to use them in place of functions (with no arguments) that act as getters, and to use closures for properties you only need to define once.

Like all properties though, be aware that it may not be immediately clear that your computed property is dynamic. With the example below:

var gradientLayerClosure: CAGradientLayer = {
    let gradientLayer = CAGradientLayer()
    return gradientLayer
  }()
  
  var gradientLayerComputed: CAGradientLayer {
    let gradientLayer = CAGradientLayer()
    return gradientLayer
  }

In our swift interface, we will get the following:

internal var gradientLayerClosure: CAGradientLayer
internal var gradientLayerComputed: CAGradientLayer { get }

Note the { get }

There is a difference in the Swift interface, but a get doesn't imply that the property is being worked out dynamically. If another developer uses a computed getter, there may be unexpected results if they are unaware that the result returned is dynamic. To reduce any risk of unexpected behaviour, ensure that your documentation is absolutely clear that it is a dynamically computed getter, although this should be the rule regardless.

We're going to continue experimenting with Swift, and we shall present more cool things we've learned.