In this episode we continue with our Starcraft II Guide application, this time focusing on using a table view to edit a model. We leverage a cell with a text field, a cell to select data from another view controller, and a section that is only editable/reorderable with itself.
Episode Links Source Code Setting up the Table View This table view will be a mixture of static & dynamic content, so we'll set it all up in code. To make things a little easier, we'll use a Swift enum to represent our different sections. enum Sections : Int { case Details = 0 case BuildOrder case Actions case Count var title: String? { switch self { case .Details: return "Guide Details" case .BuildOrder: return "Build Order" default: return nil } } } Here you can see that we have a simple Int enum that shows our sections in the right order. The Count case is there as a neat trick to sum the number of items above it. We also use a title property that will return a title for a given section. With this in place our table view data source methods are a little cleaner: override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return Sections.Count.rawValue } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let s = Sections(rawValue: section) else { return 0 } switch s { case .Details: return 2 case .BuildOrder: return guide.buildOrder.count case .Actions: return 1 default: return 0 } } override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return Sections(rawValue: section)?.title } TextField Cell To capture the name of the guide, we'll use a TextFieldCell, which is a simple UITableViewCell subclass that adds a textField to the right side. We won't keep a reference to the text field, instead we'll just read the value off of the model and update the model when the text field changes. override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { switch (indexPath.section, indexPath.row) { case (Sections.Details.rawValue, 0): let cell = TextFieldCell() cell.backgroundColor = Theme.Colors.Foreground.color cell.label.textColor = Theme.Colors.LightTextColor.color cell.label.text = "Name" cell.textField.delegate = self cell.textField.textColor = Theme.Colors.LightTextColor.color cell.textField.text = guide.name return cell // code omitted } } // MARK: UITextFieldDelegate func textFieldDidEndEditing(textField: UITextField) { guide.name = textField.text } func textFieldShouldReturn(textField: UITextField) -> Bool { textField.resignFirstResponder() return false } Selecting a Race This one will use a standard cell with the .Value1 style. We'll use a disclosure indicator to indicate that we'll select a value from another view controller. When the user taps on it, we'll present the view controller and handle the selection with a simple callback block. // cellForRowAtIndexPath: case (Sections.Details.rawValue, 1): let cell = UITableViewCell(style: .Value1, reuseIdentifier: nil) cell.backgroundColor = Theme.Colors.Foreground.color cell.textLabel?.text = "Race" cell.textLabel?.textColor = Theme.Colors.LightTextColor.color cell.textLabel?.font = UIFont.boldSystemFontOfSize(16) cell.detailTextLabel?.text = guide.race?.rawValue cell.detailTextLabel?.textColor = Theme.Colors.LightTextColor.color cell.accessoryType = .DisclosureIndicator return cell // snip override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { switch (indexPath.section, indexPath.row) { case (Sections.Details.rawValue, 1): let raceVC = storyboard!.instantiateViewControllerWithIdentifier("SelectRaceViewController") as! SelectRaceViewController raceVC.raceSelectionBlock = { race in self.guide.race = race self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None) self.navigationController?.popViewControllerAnimated(true) } self.navigationController?.pushViewController(raceVC, animated: true) default: break } Selecting a Unit For the build order section we'll append units as the user selects them. case (Sections.BuildOrder.rawValue, let i): let unit = guide.buildOrder[i] let cell = tableView.dequeueReusableCellWithIdentifier(UnitCell.reuseIdentifier, forIndexPath: indexPath) as! UnitCell cell.selectionStyle = .None if let imgName = unit.imageName, image = UIImage(named: imgName) { cell.imageView?.image = image } else { cell.imageView?.image = nil } cell.textLabel?.text = unit.name return cell Here we are using reusable cells since there will be many of these. When you tap on "Add a Unit" we'll call selectUnit(): func selectUnit() { if let race = guide.race { let unitVC = storyboard!.instantiateViewControllerWithIdentifier("SelectUnitViewController") as! SelectUnitViewController unitVC.units = units[race]! unitVC.unitSelectionBlock = { unit in self.guide.buildOrder.append(unit) let rowIndex = self.guide.buildOrder.count - 1 let indexPath = NSIndexPath(forRow: rowIndex, inSection: 1) self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) self.navigationController?.popViewControllerAnimated(true) } self.navigationController?.pushViewController(unitVC, animated: true) } else { let alert = UIAlertController(title: "Race not selected", message: "Please select a race first", preferredStyle: .Alert) alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil)) presentViewController(alert, animated: true, completion: nil) } } Supporting Reorderable Cells just for the Build Order Section First we need to tell the table view that it is in edit mode. We also need to allow selection during editing so the other table view cells still function: // viewDidLoad tableView.editing = true tableView.allowsSelectionDuringEditing = true Then we need to tell the table view that we only want editing / reordering for the build order section: override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { return indexPath.section == Sections.BuildOrder.rawValue } override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool { return indexPath.section == Sections.BuildOrder.rawValue } override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) { let unit = guide.buildOrder.removeAtIndex(sourceIndexPath.row) guide.buildOrder.insert(unit, atIndex: destinationIndexPath.row) } This works, but it allows us to reorder a unit cell outside of its section! To fix this we need to implement 1 more delegate method: override func tableView(tableView: UITableView, targetIndexPathForMoveFromRowAtIndexPath sourceIndexPath: NSIndexPath, toProposedIndexPath proposedDestinationIndexPath: NSIndexPath) -> NSIndexPath { if proposedDestinationIndexPath.section != sourceIndexPath.section { if proposedDestinationIndexPath.section < sourceIndexPath.section { return NSIndexPath(forRow: 0, inSection: sourceIndexPath.section) } else { let lastRow = self.tableView(tableView, numberOfRowsInSection: sourceIndexPath.section) - 1 return NSIndexPath(forRow: lastRow, inSection: sourceIndexPath.section) } } return proposedDestinationIndexPath } With this in place we're always using an indexPath in the build order section, so it works as you would expect. Credits Icons and unit data were obtained from the Starcraft Wikia.