Slide 1

Slide 1 text

Whistle Stop Swift Creating Apps Without Interface Builder Simon Gladman for London Swift Dev Community November 2014

Slide 2

Slide 2 text

Biography • Almost twenty years of application development • Mainly advertising production and investment banking • Campaign management, media booking, time and expenses, FX trading, business activity monitoring and approval systems • ColdFusion, MS SQL, JavaScript, ActionScript 3, Apache Flex

Slide 3

Slide 3 text

• Regular blogger: flexmonkey.blogspot.com • Twitter: @flexmonkey • Github: https://github.com/flexmonkey • Creator of Nodality • Node based image editing and compositing for iPad

Slide 4

Slide 4 text

https://github.com/FlexMonkey/ LondonSwiftDemo

Slide 5

Slide 5 text

The demo app A colour selection app for iPad that allows users to: • Create colours from red, green and blue sliders. • Select colours from a collection of presets. • Save and name their own colours.

Slide 6

Slide 6 text

• The application contains four main components • A ColorPicker - contains a UIPickerView for selecting from a list of preset colours • An RGBWidget - contains three UISliders for setting the red, green and blue components • A SavedColorsGrid - contains a UICollectionView to display the user’s selected colours • A ColorSwatch to display the current colour

Slide 7

Slide 7 text

• The main value object used is a NamedColor • It has properties for its color and name • It has a constant, immutable uuid let uuid = NSUUID().UUIDString • It is equatable via a custom operator func == (lhs: NamedColor, rhs: NamedColor) -> Bool { return lhs.uuid == rhs.uuid }

Slide 8

Slide 8 text

Landscape layout • I wanted the application to lay itself out differently when the iPad is rotated. • In landscape, I’ve broken the screen up into a grid based on the rule of thirds.

Slide 9

Slide 9 text

Portrait layout • In portrait, the four components are stacked vertically, each taking up a quarter of the available space.

Slide 10

Slide 10 text

Laying out in code • My own preference not to use Xcode’s Interface Builder and create and layout components in Swift • The advantage is I know what my code is doing and I don’t need to tweak generated code. • The disadvantage is that I need to keep running in the simulator to see how my interface looks.

Slide 11

Slide 11 text

Creating components • My components are declared as constants in the view controller let colorSwatch =ColorSwatch(frame: CGRectZero) let toolbar = UIToolbar(frame: CGRectZero) let colorPicker = ColorPicker(frame: CGRectZero) let rgbWidget = RGBWidget(frame: CGRectZero) let savedColorsGrid = SavedColorsGrid(frame: CGRectZero) • They’re added as sub-views in viewDidLoad() override func viewDidLoad() { super.viewDidLoad() view.addSubview(colorSwatch) view.addSubview(colorPicker) view.addSubview(rgbWidget) view.addSubview(savedColorsGrid) view.addSubview(toolbar) […]

Slide 12

Slide 12 text

Laying out components • Now the components are available, I define their frames inside an overridden viewDidLayoutSubviews() override func viewDidLayoutSubviews() { if view.frame.isLandscape() { // do landscape layout } else { // do portrait layout } } }

Slide 13

Slide 13 text

isLandscape() ? • A view is a CGRect which doesn’t have an isLandscape() method • I used a Swift extension to add that functionality extension CGRect { func isLandscape() -> Bool { return self.width > self.height } } • Segregates responsibility and helps make code self documenting

Slide 14

Slide 14 text

A look at the landscape layout code let toolbarHeight: CGFloat = 50 let contentHeight = view.frame.height - toolbarHeight let margin = CGFloat(10) if view.frame.isLandscape() { let leftColumnWidth = view.frame.width * 0.34 let rightColumnWidth = view.frame.width * 0.66 let upperRowHeight = contentHeight * 0.66 let bottomRowHeight = contentHeight * 0.34 - topLayoutGuide.length let upperRowTop = topLayoutGuide.length let bottomRowTop = upperRowHeight + upperRowTop savedColorsGrid.frame = CGRect(x: 0, y: upperRowTop, width: leftColumnWidth, height: upperRowHeight).rectByInsetting(dx: margin, dy: margin) colorSwatch.frame = CGRect(x: leftColumnWidth, y: upperRowTop, width: rightColumnWidth, height: upperRowHeight).rectByInsetting(dx: margin, dy: margin) colorPicker.frame = CGRect(x: 0, y: bottomRowTop, width: leftColumnWidth, height: bottomRowHeight).rectByInsetting(dx: margin, dy: margin) rgbWidget.frame = CGRect(x: leftColumnWidth, y: bottomRowTop, width: rightColumnWidth, height: bottomRowHeight).rectByInsetting(dx: margin, dy: margin) }

Slide 15

Slide 15 text

All the components share the same basic visual properties • Shadow • Border • Corner Radius

Slide 16

Slide 16 text

The Panel base component Rather than duplicating the common visual code in each of the four components, I created a Panel class which each extends. class Panel: UIControl { override init(frame: CGRect) { super.init(frame: frame) layer.backgroundColor = UIColor.lightGrayColor().CGColor layer.shadowColor = UIColor.blackColor().CGColor layer.shadowOffset = CGSize(width: 0, height: 0) layer.shadowOpacity = 0.5 layer.cornerRadius = 5 layer.borderColor = UIColor.darkGrayColor().CGColor layer.borderWidth = 1 layer.cornerRadius = 5 } }

Slide 17

Slide 17 text

The ColorSwatch

Slide 18

Slide 18 text

The simplest of the components - simply sets its own layer’s background colour via a didSet observer on its currentColor property: class ColorSwatch: Panel { var currentColor:UIColor = UIColor.blackColor() { didSet { layer.backgroundColor = currentColor.CGColor } } }

Slide 19

Slide 19 text

The RGBWidget

Slide 20

Slide 20 text

• Contains three SliderWidgets for changing red, green and blue values of a colour. • These are created and held in an array in init(): redWidget = SliderWidget(frame: CGRectZero) greenWidget = SliderWidget(frame: CGRectZero) blueWidget = SliderWidget(frame: CGRectZero) widgets = [redWidget, greenWidget, blueWidget] • Then added as sub-views by looping over the array: override func didMoveToSuperview() { for (i: Int, widget: SliderWidget) in enumerate(widgets) { addSubview(widget) widget.title = widgetTitles[i] widget.addTarget(self, action: "sliderChangeHandler", forControlEvents: UIControlEvents.ValueChanged) } } • When any slider is changed, the sliderChangeHandler() is invoked.

Slide 21

Slide 21 text

The SliderWidget is also a custom component that contains a UILabel and a UISlider let slider = UISlider(frame: CGRectZero) let label = UILabel(frame: CGRectZero) […] override func didMoveToSuperview() { slider.addTarget(self, action: "sliderChangeHandler", forControlEvents: .ValueChanged) layer.cornerRadius = 5 layer.backgroundColor = UIColor.darkGrayColor().colorWithAlphaComponent(0.25).CGColor addSubview(slider) addSubview(label) } override func layoutSubviews() { label.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height / 2).rectByInsetting(dx: 5, dy: 5) slider.frame = CGRect(x: 0, y: frame.height / 2, width: frame.width, height: frame.height / 2).rectByInsetting(dx: 5, dy: 5) }

Slide 22

Slide 22 text

When either its value or title are changed, it updates its slider and label: var title: String = "" { didSet { updateLabel() } } var value: Float = 0 { didSet { slider.value = value updateLabel() } } func updateLabel() { label.text = title + ": " + value.format() }

Slide 23

Slide 23 text

The slider widgets are also laid out using code: override func layoutSubviews() { let widgetHeight = frame.height / 3 let margin = CGFloat(6) for (i: Int, widget: SliderWidget) in enumerate(widgets) { widget.frame = CGRect(x: 0, y: CGFloat(i) * widgetHeight, width: frame.width, height: widgetHeight).rectByInsetting(dx: margin, dy: margin) } }

Slide 24

Slide 24 text

• The handler changes the component’s currentColor property: func sliderChangeHandler() { enableObserversOnColorComponents = false currentColor = UIColor.colorFromFloats(redComponent: redWidget.value, greenComponent: greenWidget.value, blueComponent: blueWidget.value) enableObserversOnColorComponents = true } • currentColor uses a didSet observer to set the individual color components and dispatch its own control event: var currentColor: UIColor = UIColor.blackColor() { didSet { redComponent = currentColor.getRGB().redComponent greenComponent = currentColor.getRGB().greenComponent blueComponent = currentColor.getRGB().blueComponent sendActionsForControlEvents(UIControlEvents.ValueChanged) } } • Those individual components set the respective sliders - if the current colour hasn’t been set by a slider: private var greenComponent: Float = 0 { didSet { if enableObserversOnColorComponents { greenWidget.value = greenComponent } } }

Slide 25

Slide 25 text

getRGB() and colorFromFloats() Two methods on UIColor you won’t have seen before - both added as extensions class func colorFromFloats(#redComponent: Float, greenComponent: Float, blueComponent: Float) -> UIColor { return UIColor(red: CGFloat(redComponent), green: CGFloat(greenComponent), blue: CGFloat(blueComponent), alpha: 1.0) } func getRGB() -> (redComponent: Float, greenComponent: Float, blueComponent: Float) { let colorRef = CGColorGetComponents(self.CGColor); let redComponent = Float(colorRef[0]) let greenComponent = Float(colorRef[1]) let blueComponent = Float(colorRef[2]) return (redComponent: redComponent, greenComponent: greenComponent, blueComponent: blueComponent) }

Slide 26

Slide 26 text

The ColorPicker

Slide 27

Slide 27 text

• The ColorPicker contains a UIPickerView and acts as both UIPickerViewDataSource and UIPickerViewDelegate • It contains an immutable array of preset colours • It has a currentColor property which refers to the selected colour which is set: • From the picker itself • When the user changes the sliders • When the user selects a saved color

Slide 28

Slide 28 text

• UIPickerViewDataSource is used to define the number of rows in the picker func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return colors.count } • UIPickerViewDelegate is used to define the item renderer: func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView!) -> UIView { let rendererColor = (row == 0) ? NamedColor(name: "Custom", color: currentColor) : colors[row] let namedColorRenderer = NamedColorItemRenderer(frame: CGRect(x: 0, y: 0, width: frame.width, height: rowHeight)) namedColorRenderer.editable = false namedColorRenderer.namedColor = rendererColor return namedColorRenderer } • …and to report a change in selection: func pickerView(pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int) { currentColor = colors[row].color }

Slide 29

Slide 29 text

When the ColorPicker’s current colour is changed, either internally or externally, it dispatches a control event and attempts to find a match in its array of presets. If there’s no match, it sets the picker to the ‘custom colour’ item. var currentColor : UIColor = UIColor.blackColor() { didSet { sendActionsForControlEvents(.ValueChanged) var matchFound = false if let _colorIndex = findColorInNamedColors(colors, currentColor) { spinner.selectRow(_colorIndex, inComponent: 0, animated: true) matchFound = true } if !matchFound { spinner.selectRow(0, inComponent: 0, animated: true) spinner.reloadComponent(0) } } }

Slide 30

Slide 30 text

The SavedColorsGrid

Slide 31

Slide 31 text

• The SavedColorsGrid contains a UICollectionView and acts as its UICollectionViewDataSource and UICollectionViewDelegate • It has a colors array which is a collection of the colours saved by the user • It also supports a ‘tap-hold’ gesture to pop up a little menu to delete saved colours

Slide 32

Slide 32 text

Both the picker and the collection view use the same item renderer, NamedColorItemRenderer, but the collection view’s code is slightly different. • I need to register the renderer class: uiCollectionView.registerClass(NamedColorItemRenderer.self, forCellWithReuseIdentifier: "Cell") • Then use one of the delegate functions to define the cell for a given index: func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as NamedColorItemRenderer cell.hasBackground = true cell.editable = true cell.namedColor = colors[indexPath.item] return cell }

Slide 33

Slide 33 text

When the SavedColorsGrid’s colors property is changed, I want any changes, e.g. additions or deletions, to animate nicely. var colors: [NamedColor] = [NamedColor]() { didSet { if oldValue.count < colors.count && colors.count - oldValue.count == 1 && oldValue.count != 0 { for newColor in colors { if find(oldValue, newColor) == nil { let newIndex = find(colors, newColor) let insertIndexPath = NSIndexPath(forItem: newIndex!, inSection: 0) uiCollectionView.insertItemsAtIndexPaths([insertIndexPath]) } } } else if oldValue.count > colors.count { for color in oldValue { if find(colors, color) == nil { let deletedIndex = find(oldValue, color) let deleteIndexPath = NSIndexPath(forItem: deletedIndex!, inSection: 0) uiCollectionView.deleteItemsAtIndexPaths([deleteIndexPath]) } } } else { uiCollectionView.reloadData() } } }

Slide 34

Slide 34 text

By implementing the action related functions in UICollectionViewDelegate, I can pop up the ‘cut’ option to delete colors func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool { return true } func collectionView(collectionView: UICollectionView, canPerformAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject!) -> Bool { return action.description == "cut:" } func collectionView(collectionView: UICollectionView, performAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject!) { if action.description == "cut:" { colors = colors.filter({$0 != self.colors[indexPath.item]}) sendActionsForControlEvents(UIControlEvents.SavedColorDeleted) } }

Slide 35

Slide 35 text

The NamedColorItemRenderer • The item renderer used in both the picker and collection view • Contains an editable text input, a readonly label and a colour swatch • When its namedColor property changes, the sub-components are updated using a didSet observer: var namedColor: NamedColor? { didSet { if let _namedColor = namedColor { label.text = _namedColor.name textInput.text = _namedColor.name swatch.backgroundColor = _namedColor.color } } }

Slide 36

Slide 36 text

• Switches between a read only label or an editable text input by a didSet observer on its editable property: var editable: Bool = false { didSet { if editable { label.removeFromSuperview() addSubview(textInput) } else { textInput.removeFromSuperview() addSubview(label) } } }

Slide 37

Slide 37 text

• Because it is the text field’s UITextFieldDelegate, it can respond to edits. • Because NamedColor is a class, the renderer holds a reference to it and changes to it are available up in the view controller func textFieldShouldReturn(textField: UITextField) -> Bool { textField.endEditing(true) return true } func textFieldDidEndEditing(textField: UITextField) { if namedColor != nil { namedColor!.name = textInput.text } }

Slide 38

Slide 38 text

The toolbar

Slide 39

Slide 39 text

• The toolbar is an instance of UIToolbar that’s added as a sub-view and laid out in the view controller. • The toolbar contains three items • Save • Tweak • About • Which are added in populateToolbar() func populateToolbar() { let saveBarButtonItem = UIBarButtonItem(title: "Save", style: UIBarButtonItemStyle.Plain, target: self, action: "saveCurrentColor") let tweakBarButtonItem = UIBarButtonItem(title: "Tweak", style: UIBarButtonItemStyle.Plain, target: self, action: "showTweakMenu:") let aboutBarButtonItem = UIBarButtonItem(title: "About", style: UIBarButtonItemStyle.Plain, target: self, action: "showAbout") let spacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FlexibleSpace, target: nil, action: nil) toolbar.setItems([saveBarButtonItem, spacer, tweakBarButtonItem, spacer, aboutBarButtonItem], animated: true) }

Slide 40

Slide 40 text

“Tweak” • The “tweak” toolbar button pops up a small alert view. • In the view controller’s init() function, I create an alert controller of style ActionSheet and give it some actions: alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.ActionSheet) makeDarkerAlertAction = UIAlertAction(title: "Make darker", style: UIAlertActionStyle.Default, handler: adjustColor) makeDarkerLighterAction = UIAlertAction(title: "Make lighter", style: UIAlertActionStyle.Default, handler: adjustColor) alertController.addAction(makeDarkerLighterAction) alertController.addAction(makeDarkerAlertAction) • To display the menu, I invoke presentViewController(): func showTweakMenu(value: UIBarButtonItem) { alertController.popoverPresentationController?.barButtonItem = value presentViewController(alertController, animated: true, completion: nil) }

Slide 41

Slide 41 text

“About” • “About” also uses a UIAlertController, but this time of style Alert: func showAbout() { var alertController = UIAlertController(title: "London Swift Demo Application", message: "Simon Gladman | November 2014", preferredStyle: UIAlertControllerStyle.Alert) let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil) let openBlogAction = UIAlertAction(title: "Open Blog", style: .Default, handler: visitFlexMonkey) alertController.addAction(okAction) alertController.addAction(openBlogAction) presentViewController(alertController, animated: true, completion: nil) }

Slide 42

Slide 42 text

Wiring up

Slide 43

Slide 43 text

• All the interactive components dispatch UIControlEvents and the view controller listens for these: rgbWidget.addTarget(self, action: "rgbWidgetChangeHandler", forControlEvents: UIControlEvents.ValueChanged) colorPicker.addTarget(self, action: "colorPickerChangeHandler", forControlEvents: UIControlEvents.ValueChanged) savedColorsGrid.addTarget(self, action: "savedColorsGridSelectHandler", forControlEvents: UIControlEvents.SavedColorSelected) • These three events do pretty much the same thing: change the view controller’s own currentColor: func colorPickerChangeHandler() { currentColor = colorPicker.currentColor } func rgbWidgetChangeHandler() { currentColor = rgbWidget.currentColor } func savedColorsGridSelectHandler() { currentColor = savedColorsGrid.getSelectedColor() }

Slide 44

Slide 44 text

• The view controller’s currentColor has a didSet observer that sets the currentColor on the control that hasn’t changed: var currentColor: UIColor = UIColor.brownColor() { didSet { if !(rgbWidget.currentColor == currentColor) { rgbWidget.currentColor = currentColor } if !(colorPicker.currentColor == currentColor) { colorPicker.currentColor = currentColor } colorSwatch.currentColor = currentColor } }

Slide 45

Slide 45 text

• The view controller also has a mutable savedColors array which contains all the colours the user has saved • A didSet() observer of this array passes it into the saved colours grid when changed var savedColors: [NamedColor] = [NamedColor]() { didSet { savedColorsGrid.colors = savedColors } }

Slide 46

Slide 46 text

Persisting Data with Core Data • Core Data allows the application to persist data, the saved user defined colours, after restarting. • If you forget to check the ‘use Core Data’ option, create a new project with that checked and copy the guts of AppDelegate into your existing project.

Slide 47

Slide 47 text

Create an entity • Create a new Core Data Data Model class. • Define the properties of the entity. • Create an NSManagedObject version of the entity.

Slide 48

Slide 48 text

• The generated code in AppDelegate contains all the boilerplate code to get started. • I create a context in the view controller: appDelegate = UIApplication.sharedApplication().delegate as AppDelegate managedObjectContext = appDelegate.managedObjectContext! • To load saved items from a previous session, invoke executeFetchRequest on the context: fetchResults = managedObjectContext.executeFetchRequest(fetchRequest, error: nil)

Slide 49

Slide 49 text

• For this application, I want to cast the results as an array of NamedColor and convert each retrieved item as an instance of NamedColor func loadSavedColors() { let fetchRequest = NSFetchRequest(entityName: "NamedColorEntity") if let fetchResults = managedObjectContext.executeFetchRequest(fetchRequest, error: nil) as? [NamedColorEntity] { var loadedColors = [NamedColor]() for namedColorEntity in fetchResults { loadedColors.append(NamedColorEntity.createInstanceFromEntity(namedColorEntity)) } savedColors = loadedColors } } class func createInstanceFromEntity(entity: NamedColorEntity) -> NamedColor! { let name = entity.colorName let color = UIColor.colorFromNSNumbers(redComponent: entity.redComponent, greenComponent: entity.greenComponent, blueComponent: entity.blueComponent) var namedColor = NamedColor(name: name, color: color) namedColor.entityRef = entity return namedColor }

Slide 50

Slide 50 text

• To save a new user defined color, I do the opposite: create a DTO from an instance of NamedColor and invoke insertNewObjectForEntityForName() func saveCurrentColor() { let savedColor = NamedColor(name: currentColor.getHex(), color: currentColor) savedColors.insert(savedColor, atIndex: 0) NamedColorEntity.createInManagedObjectContext(managedObjectContext, namedColor: savedColor) appDelegate.saveContext() } class func createInManagedObjectContext(moc: NSManagedObjectContext, namedColor: NamedColor) -> NamedColorEntity { let newEntity = NSEntityDescription.insertNewObjectForEntityForName("NamedColorEntity", inManagedObjectContext: moc) as NamedColorEntity let colorComponents = namedColor.color.getRGB() newEntity.redComponent = colorComponents.redComponent newEntity.greenComponent = colorComponents.greenComponent newEntity.blueComponent = colorComponents.blueComponent newEntity.colorName = namedColor.name namedColor.entityRef = newEntity return newEntity }

Slide 51

Slide 51 text

• Because I have a reference to the entity inside NamedColor, I can persist edits inside the domain object itself: var name : String { didSet { if let _entityRef = entityRef { _entityRef.colorName = name let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate appDelegate.saveContext() } } } • This could be considered a little naughty or could be considered “Domain Driven Development”

Slide 52

Slide 52 text

• The saved colours grid dispatches a delete event. • To delete in Core Data, I compare the saved colours grid’s colors array to the view controller’s and invoke deleteObject() on the missing one: func savedColorsGridDeleteHandler() { for color in savedColors { if find(savedColorsGrid.colors, color) == nil { if let _entityRef = color.entityRef { managedObjectContext.deleteObject(_entityRef) } } } appDelegate.saveContext() savedColors = savedColorsGrid.colors }

Slide 53

Slide 53 text

We’re all done! Questions are always welcome, as are gin and tonics.