Saturday, September 30, 2017

UITableView Demo

I'm using Xcode 8 and Swift 3 to build this demo application I got from the book "iOS Programming - The Big Nerd Ranch Guide". The app uses a table view to display a list of items grouped into 2 sections: one for the items worth more than $50 and another one for the rest. The image below shows how it will looks like.



Below is the list of files required by this project:




The main classes involved in this project are:
UITableViewController : retrieves data from the data source to display in the table view
- UITableView : displays the data
- UITableViewDelegate : is a protocol adopted by the delegate of the UITableView object. The table view asks the delegate for information such as row's height and level of indentation and to perform some actions (managing selections and modifying header and footer) for certain events.
- UITableViewCell : is constructed and filled will data by the UITableViewDataSource and then is passed to the table view to create a row.
- UITableViewDataSource : (protocol) is a representative of the data model which provides minimal information about the table view's appearance (such as number of sections and number of rows in each section).


1. Create a single view application in Xcode


- Suppose you already created the a single view application in Xcode named Homepwner. Then in Interface Builder, delete the view controller that Xcode created by default and drag the Table View Controller from the Object Library to the Interface Builder. Here is how it looks like.



- Open Attributes Inspector and check "Is Initial View Controller" under View Controller section.

- Select the white area under the Property Cells and open Attributes Inspector. Then, change the Style in Table View Cell section to Right Detail, which corresponds to UITableViewCellStyle.value1. After that, change the Identifier to UITableViewCell.

- Select the Property Cells and open Attributes Inspector. Then, change the Style in Table View section to Grouped.

After these steps, the view controller would look like following:


2. Create data model classes


The Item class inherits from NSObject. It's because all of UIKit classes such as UIView, UITextField, and UIViewController, inherit either directly or indirectly from NSObject. If at some points you need to interface with the runtime system (or directly call Objective-C functions, which require NSObject), you would be fine. For example, the Objective-C function class_copyPropertyList() accepts NSObject object as its argument (just import this framework ObjectiveC.runtime and then you can call that function in your Swift codes).

ItemStore class is just a helper class so it doesn't have to inherits from NSObject.

Item.swift

import UIKit
class Item: NSObject {
    var name: String
    var valueInDollars: Int
    var serialNumber: String?
    let dateCreated: NSDate
    
    init(_ name: String, _ serialNumber: String?, _ valueInDollars: Int) {
        self.name = name
        self.serialNumber = serialNumber
        self.valueInDollars = valueInDollars
        self.dateCreated = NSDate()
        
        super.init()
    }
    
    convenience init(_ random: Bool = false) {
        if random {
            let adjectives = ["Fluffy", "Rusty", "Shiny"]
            let nouns = ["Bear", "Spork", "Mac"]
            
            let idx = Int(arc4random_uniform(UInt32(adjectives.count)))
            let randomNoun = nouns[idx]
            let randomAdjective = adjectives[idx]
            
            let randomName = "\(randomAdjective) \(randomNoun)"
            let randomSerialNumber = NSUUID().uuidString.components(separatedBy: "-").first!
            let randomValueInDollars = Int(arc4random_uniform(100))
            
            self.init(randomName, randomSerialNumber, randomValueInDollars)
            return
        }
        
        self.init("", nil, 0)
    }
    
    func toString() -> String {
        return "Item: name=\(name), serialNumber=\(String(describing: serialNumber)), valueInDollars=\(valueInDollars), dateCreated=\(dateCreated)"
    }
}

ItemStore.swift 



class ItemStore {
    var allItems = [Item]()
    
    init() {
        createItem(valueInDollars: 20)
        createItem(valueInDollars: 30)
        createItem(valueInDollars: 60)
        createItem(valueInDollars: 70)
        createItem(valueInDollars: 80)
        
    }
    
    func createItem() -> Item {
        let item = Item(true)
        allItems.append(item)
        return item
    }
    
    func createItem(valueInDollars: Int) -> Item {
        let item = Item(true)
        item.valueInDollars = valueInDollars
        allItems.append(item)
        return item
    }
    
    func getGreaterThan50DollarsItems() -> [Item] {
        var items = [Item]()
        for item in allItems {
            if item.valueInDollars > 50 {
                items.append(item)
            }
        }
        return items
    }
    
    func getLessThanOrEqualsTo50DollarsItems() -> [Item] {
        var items = [Item]()
        for item in allItems {
            if item.valueInDollars <= 50 {
                items.append(item)
            }
        }
        return items
    }
}

3. Implement view controller 


3.1 Create ItemsViewController.swift file




import UIKit
class ItemsViewController: UITableViewController {
    
    var itemStore: ItemStore!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)
        tableView.contentInset = insets
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print("\nsection=\(section)")
        
        switch section {
        case 0:
            return itemStore.getGreaterThan50DollarsItems().count
        case 1:
            return itemStore.getLessThanOrEqualsTo50DollarsItems().count
        default:
            return 0
        }
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//        let cell = UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "UITableViewCell")
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell")!
        
        let items: [Item]
        switch indexPath.section {
        case 0:
            items = itemStore.getGreaterThan50DollarsItems()
        case 1:
            items = itemStore.getLessThanOrEqualsTo50DollarsItems()
        default:
            items = [Item]()
        }
        
        let item = items[indexPath.row]
        cell.textLabel?.text = item.name
        cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        
        print("\n\(item.toString())")
        print("\nrow=\(indexPath.row), section=\(indexPath.section)")
        return cell
    }
}

- I overrode the numberOfSections(tableView) method of UITableViewDataSource protocol to tell the table view that there are two sections.

- The method tableView(_:numberOfRowsInSection:) tells the table view the number of rows in each section.

- The method tableView(_:cellForRowAt) constructs a cell with data for a specific row and section then returns it to the table view. Please note that if we have 2 sections and the first section has 3 rows and the second section has 2 rows, this method gets called 5 times. If the first section has 3 rows and the second section does not have a row at all, this method gets called only 3 times.

- Calling this method tableView.dequeueReusableCell(withIdentifier: "UITableViewCell") retrieves the existing UITableViewCell instance from the queue to avoid creating a new one every time the method tableView(_:cellForRowAt) is called. As all cells have the same style, we create it once and reuse it and just change the data. It saves memory. The cell was configured in step 1 and will be created and put in to queue when the application launches.

3.2 Update the table view controller's class in Interface Builder


Open Main.storyboard file and select the table view controller then open the Identity Inspector and change the Class setting under Custom Class section to ItemsViewController.


4. Construct data


Open AppDelegate.swift file and add the following lines to the method application(_:didFinishLaunchingWithOptions)



        var itemStore = ItemStore()
        let itemsController = window!.rootViewController as! ItemsViewController
        itemsController.itemStore = itemStore
        

Constructing data is not the role of ItemsViewController class so I instantiated the ItemStore in AppDelegate.swift and injected it to the itemStore property. This also respects to dependency injection principle. If the way to create the ItemStore changes, we don't have to modify the ItemsViewController class. 





No comments:

Post a Comment