Now that I know that MVC can help do better code, I want to make my SettingsTableViewController
class conform to it.
SettingsTableViewController
is a subclass of UITableViewController
. It is linked to a storyboard scene that contains a UITableView
with two cells (grouped tableView style, prototype cells). The first cell has a right detail style (textLabel and detailTextLabel), the second cell has a custom style and displays a UIPickerView
inside of its contentView.
When it appears on screen, SettingsTableViewController
displays only the first cell. When I click on this cell, the second cell containing a UIPickerView
appears. If I click again on the first cell, the second cell disappears.
The detailtextLabel of the first row displays an integer saved in NSUserDefaults
. The pickerView of the second cell is linked to an array of integers. When I select a row in the pickerView, it saves the row's related integer in NSUserDefaults
and updates the detailtextLabel of the first cell.
The image below may help understand the way it works:
Before knowing the concept of MVC, I was able to write the following code to make the previous explanation work:
class SettingsTableViewController: UITableViewController, UIPickerViewDelegate, UIPickerViewDataSource {
let itemsArray = [5, 10, 15]
var pickerIndexPath: NSIndexPath? //acts like a Bool and allows to hide or show cell with identifier "pickerCell"
var numberOfItems: Int {
get {
return NSUserDefaults.standardUserDefaults().integerForKey("NumberOfItems")
}
set {
NSUserDefaults.standardUserDefaults().setInteger(newValue, forKey: "NumberOfItems")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Settings"
//Init numberOfItems
if numberOfItems == 0 {
numberOfItems = 5
}
//Autoset cells height (iOS8)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//If pickerIndexPath is nil, show only one cell
return pickerIndexPath == nil ? 1 : 2
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell!
if indexPath.row == 0 {
cell = tableView.dequeueReusableCellWithIdentifier("RightDetailCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = "Items"
cell.detailTextLabel?.text = "\(numberOfItems)"
} else {
cell = tableView.dequeueReusableCellWithIdentifier("PickerCell", forIndexPath: indexPath) as UITableViewCell
cell.selectionStyle = .None
//Set pickerView inside cell
let picker = cell.viewWithTag(1) as UIPickerView
picker.delegate = self
picker.dataSource = self
//Set the middle row in picker according to numberOfItems
if let index = find(itemsArray, numberOfItems) {
picker.selectRow(index, inComponent: 0, animated: false)
}
}
return cell
}
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
switch indexPath {
case NSIndexPath(forRow: 1, inSection: 0):
return nil
default:
return indexPath
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
if indexPath == NSIndexPath(forRow: 0, inSection: 0) {
//Show or hide pickerCell
if pickerIndexPath == nil {
tableView.beginUpdates()
pickerIndexPath = NSIndexPath(forRow: 1, inSection: 0)
tableView.insertRowsAtIndexPaths([pickerIndexPath!], withRowAnimation: .Fade)
tableView.endUpdates()
} else {
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([pickerIndexPath!], withRowAnimation: .Fade)
pickerIndexPath = nil
tableView.endUpdates()
}
}
}
//MARK: UIPickerViewDataSource
func numberOfComponentsInPickerView(_: UIPickerView) -> Int {
return 1
}
func pickerView(_: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return itemsArray.count
}
//MARK: UIPickerViewDelegate
func pickerView(_: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return "\(itemsArray[row]) items"
}
func pickerView(_: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if pickerIndexPath != nil {
//save new value in NSUserDefaults
numberOfItems = itemsArray[row]
//update first cell
let index = NSIndexPath(forRow: pickerIndexPath!.row - 1, inSection: 0)
tableView.reloadRowsAtIndexPaths([index], withRowAnimation: .Fade)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Then, in order to conform to MVC design pattern, I replaced the previous code with the following:
SettingsTableViewController:
class SettingsTableViewController: UITableViewController {
let dataSource = DataSource()
override func viewDidLoad() {
super.viewDidLoad()
title = "Settings"
//Autoset cells height (iOS8)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//If pickerIndexPath is nil, show only one cell
return dataSource.pickerIndexPath == nil ? 1 : 2
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("LabelCell", forIndexPath: indexPath) as LabelCell
cell.dataSource = dataSource
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("PickerCell", forIndexPath: indexPath) as PickerCell
cell.dataSource = dataSource
return cell
}
}
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
switch indexPath {
case NSIndexPath(forRow: 1, inSection: 0):
return nil
default:
return indexPath
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
if indexPath == NSIndexPath(forRow: 0, inSection: 0) {
//Show or hide pickerCell
tableView.beginUpdates()
if dataSource.pickerIndexPath == nil {
dataSource.pickerIndexPath = NSIndexPath(forRow: indexPath.row + 1, inSection: 0)
tableView.insertRowsAtIndexPaths([dataSource.pickerIndexPath!], withRowAnimation: .Fade)
} else {
tableView.deleteRowsAtIndexPaths([dataSource.pickerIndexPath!], withRowAnimation: .Fade)
dataSource.pickerIndexPath = nil
}
tableView.endUpdates()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
LabelCell:
//Global context variable
private var observerContext = 0
class LabelCell: UITableViewCell {
var dataSource: DataSource? {
willSet {
disconnectFromModel()
}
didSet {
connectToModel()
update()
}
}
private func disconnectFromModel() {
dataSource?.removeObserver(self, forKeyPath: "numberOfItems", context: &observerContext)
}
private func connectToModel() {
dataSource?.addObserver(self, forKeyPath: "numberOfItems", options: NSKeyValueObservingOptions(), context: &observerContext)
}
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafeMutablePointer<Void>) {
if context == &observerContext {
update()
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
private func update() {
if let model = dataSource {
textLabel?.text = "Items"
detailTextLabel?.text = "\(model.numberOfItems)"
}
}
deinit {
disconnectFromModel()
}
}
PickerCell:
class PickerCell: UITableViewCell, UIPickerViewDelegate, UIPickerViewDataSource {
@IBOutlet weak var picker: UIPickerView!
var dataSource: DataSource? {
didSet {
update()
}
}
override func awakeFromNib() {
super.awakeFromNib()
selectionStyle = .None
picker.delegate = self
picker.dataSource = self
}
func update() {
if let dataSource = dataSource {
if let index = find(dataSource.itemsArray, dataSource.inspectionPref) {
picker.selectRow(index, inComponent: 0, animated: false)
}
}
}
//MARK: UIPickerViewDataSource
func numberOfComponentsInPickerView(_: UIPickerView) -> Int {
return 1
}
func pickerView(_: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataSource!.itemsArray.count
}
//MARK: UIPickerViewDelegate
func pickerView(_: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return "\(dataSource!.itemsArray[row]) items"
}
func pickerView(_: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
dataSource!.numberOfItems = dataSource!.itemsArray[row]
}
}
DataSource:
class DataSource: NSObject {
let itemsArray = [5, 10, 15]
var pickerIndexPath: NSIndexPath? //allows to hide or show cell with identifier "pickerCell"
dynamic var numberOfItems: Int {
get {
return NSUserDefaults.standardUserDefaults().integerForKey("NumberOfItems")
}
set {
NSUserDefaults.standardUserDefaults().setInteger(newValue, forKey: "NumberOfItems")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
override init() {
super.init()
//Init numberOfItems at first launch
if numberOfItems == 0 {
numberOfItems = 5
}
}
}
I have several questions about this new code. As it is my first attempt with MVC, I wonder if it is a real MVC design pattern code. I also ask myself if it is a complete/correct MVC code: is there anything left to do/modify in order to fully conform to MVC? Furthermore, I've read that passing a model to a view (here, a cell) is not recommended. Thus, what would be the way to make a cell interact with the model without passing the model to it?