Adding sections, separated by dates, to UITableView in Swift
Asked Answered
K

4

21

I'm a complete rookie at Swift and iOS programming so you'll have to forgive the perhaps simple question.

I've created a tableView which displays the contents of an array (strings) at the press of a button. Now, I'd like to "group" these strings in tableView sections, sorted by date.

In more detail: When the user taps the button, the string should be inserted at index 0 of the array and be displayed in a section with a header of todays date. If there's values older than today's date in the array, these should be displayed in a separate section for that date. Each section should correspond to a 24 hour day and display all the strings added during that day.

Here's some sample code of what I've achieved so far:

var testArray[String]()
var sectionsInTable[String]()

@IBOutlet weak var testTable: UITableView!

@IBAction func saveButton(sender: AnyObject) {

testArray.insert("\(strTest)", atIndex: 0)
testTable.reloaddata()

}

func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    return sectionsInTable.count

}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{

    return testArray.count

}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")

    cell.textLabel.text = String(testArray[indexPath.row])        

    return cell
}

I really don't know how to manage the sections part. Hopefully someone can point me in the right direction. Thanks!

Kowloon answered 27/10, 2014 at 20:17 Comment(0)
D
23

I would usually do this with Core Data and NSFetchedResultsController since it has built-in methods for getting sections.

However, I'll answer the question without using Core Data. The code is a little messier but here we go...

First, you have to create an object that will store both the date and the text. The testArray will be an array of these objects, instead of a String array. For example:

class DateTextItem: NSObject {
    var text: String = ""
    var insertDate: NSDate = NSDate()    
}

var testArray = [DateTextItem]()

Then when the saveButton is hit, we'll create and add the DateTextItem object. We'll also add the date to the sectionsInTable if it doesn't already exist.

@IBAction func saveButton(sender: AnyObject) {
    let newItem = DateTextItem()
    newItem.text = "Test \(testArray.count)"

    // this is for development only
    // increment the date after 2 records so we can test grouping by date
    if testArray.count >= (testArray.count/2) {
        let incrementDate = NSTimeInterval(86400*(testArray.count/2))
        newItem.insertDate = NSDate(timeIntervalSinceNow:incrementDate)
    }

    testArray.append(newItem)

    // this next bit will create a date string and check if it's in the sectionInTable
    let df = NSDateFormatter()
    df.dateFormat = "MM/dd/yyyy"
    let dateString = df.stringFromDate(newItem.insertDate)

    // create sections NSSet so we can use 'containsObject'
    let sections: NSSet = NSSet(array: sectionsInTable)

    // if sectionsInTable doesn't contain the dateString, then add it
    if !sections.containsObject(dateString) {
        sectionsInTable.append(dateString)
    }

    self.tableView.reloadData()
}

Next, I created a function to get the items in a section since we need it in a couple places.

func getSectionItems(section: Int) -> [DateTextItem] {
    var sectionItems = [DateTextItem]()

    // loop through the testArray to get the items for this sections's date
    for item in testArray {
        let dateTextItem = item as DateTextItem
        let df = NSDateFormatter()
        df.dateFormat = "MM/dd/yyyy"
        let dateString = df.stringFromDate(dateTextItem.insertDate)

        // if the item's date equals the section's date then add it
        if dateString == sectionsInTable[section] as NSString {
            sectionItems.append(dateTextItem)
        }
    }

    return sectionItems
}

Finally, here is what the Table View Data Source methods look like

// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return sectionsInTable.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.getSectionItems(section).count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    // Configure the cell...
    var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")

    // get the items in this section
    let sectionItems = self.getSectionItems(indexPath.section)
    // get the item for the row in this section
    let dateTextItem = sectionItems[indexPath.row]

    cell.textLabel.text = dateTextItem.text

    return cell
}

// print the date as the section header title
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return sectionsInTable[section]
}
Diley answered 28/10, 2014 at 18:20 Comment(2)
Thank you very much! Will try it right away and let you know how it works.Kowloon
Every time when table is scrolling - loop is working and array initialized. I am not sure that's great solution. You need to prepare list with objects once, and then use it.Archduchess
O
32

I was in need for something similar, and while Ron Fessler's solution works, when there's a lot of sections/rows, it took a very long time for table to load data, and even after that it wasn't much responsive. Main issue there I think is getSectionItems function as it will always go through all of items...

My solution:

struct TableItem {
    let title: String
    let creationDate: NSDate
}

var sections = Dictionary<String, Array<TableItem>>()
var sortedSections = [String]()

@IBAction func saveButton(sender: AnyObject) {

    let date:String = "your date in string..."

    //if we don't have section for particular date, create new one, otherwise we'll just add item to existing section
    if self.sections.indexForKey(date) == nil {
        self.sections[date] = [TableItem(title: name, creationDate: date)]
    }
    else {
        self.sections[date]!.append(TableItem(title: name, creationDate: date))
    } 

    //we are storing our sections in dictionary, so we need to sort it 
    self.sortedSections = self.sections.keys.array.sorted(>)
    self.tableView.reloadData()
}

tableView dataSource methods:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return sections.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return sections[sortedSections[section]]!.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell = tableView.dequeueReusableCellWithIdentifier("Cell")        

    let tableSection = sections[sortedSections[indexPath.section]]
    let tableItem = tableSection![indexPath.row]

    cell.titleLabel?.text = tableItem.title

    return cell
}

override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return sortedSections[section]
}
Ostracoderm answered 20/4, 2015 at 18:20 Comment(6)
This is a nice simple solution which is easy to implement on an existing TableView. Many thanks.Millicent
i keep getting a atal error: Index out of range when i use [section] in tabelview delegate ?Vestment
@Vestment check that section is actually within the range of sortedSections before return. E.g.: if section < sortedSections.count { return ... }Ostracoderm
i found it ....... func numberOfSectionsInTableView(tableView: UITableView) -> Int is remanded in swift 4 so was never calledVestment
Yep, I had the same issue since Swift 4 came out as well.Ostracoderm
good answer but there wasn't an array property on keys: sections.keys.array.sorted(>). I had to sort like this: self.sections.keys.sorted { $0 > $1 }Hypothalamus
D
23

I would usually do this with Core Data and NSFetchedResultsController since it has built-in methods for getting sections.

However, I'll answer the question without using Core Data. The code is a little messier but here we go...

First, you have to create an object that will store both the date and the text. The testArray will be an array of these objects, instead of a String array. For example:

class DateTextItem: NSObject {
    var text: String = ""
    var insertDate: NSDate = NSDate()    
}

var testArray = [DateTextItem]()

Then when the saveButton is hit, we'll create and add the DateTextItem object. We'll also add the date to the sectionsInTable if it doesn't already exist.

@IBAction func saveButton(sender: AnyObject) {
    let newItem = DateTextItem()
    newItem.text = "Test \(testArray.count)"

    // this is for development only
    // increment the date after 2 records so we can test grouping by date
    if testArray.count >= (testArray.count/2) {
        let incrementDate = NSTimeInterval(86400*(testArray.count/2))
        newItem.insertDate = NSDate(timeIntervalSinceNow:incrementDate)
    }

    testArray.append(newItem)

    // this next bit will create a date string and check if it's in the sectionInTable
    let df = NSDateFormatter()
    df.dateFormat = "MM/dd/yyyy"
    let dateString = df.stringFromDate(newItem.insertDate)

    // create sections NSSet so we can use 'containsObject'
    let sections: NSSet = NSSet(array: sectionsInTable)

    // if sectionsInTable doesn't contain the dateString, then add it
    if !sections.containsObject(dateString) {
        sectionsInTable.append(dateString)
    }

    self.tableView.reloadData()
}

Next, I created a function to get the items in a section since we need it in a couple places.

func getSectionItems(section: Int) -> [DateTextItem] {
    var sectionItems = [DateTextItem]()

    // loop through the testArray to get the items for this sections's date
    for item in testArray {
        let dateTextItem = item as DateTextItem
        let df = NSDateFormatter()
        df.dateFormat = "MM/dd/yyyy"
        let dateString = df.stringFromDate(dateTextItem.insertDate)

        // if the item's date equals the section's date then add it
        if dateString == sectionsInTable[section] as NSString {
            sectionItems.append(dateTextItem)
        }
    }

    return sectionItems
}

Finally, here is what the Table View Data Source methods look like

// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return sectionsInTable.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.getSectionItems(section).count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    // Configure the cell...
    var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")

    // get the items in this section
    let sectionItems = self.getSectionItems(indexPath.section)
    // get the item for the row in this section
    let dateTextItem = sectionItems[indexPath.row]

    cell.textLabel.text = dateTextItem.text

    return cell
}

// print the date as the section header title
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return sectionsInTable[section]
}
Diley answered 28/10, 2014 at 18:20 Comment(2)
Thank you very much! Will try it right away and let you know how it works.Kowloon
Every time when table is scrolling - loop is working and array initialized. I am not sure that's great solution. You need to prepare list with objects once, and then use it.Archduchess
S
1

You have to make an array for each day (called dayArray[] for example) and add it to sectionInTable[] and do something like that :

func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    return sectionsInTable.count

}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{

    return sectionsInTable.objectAtIndex(section).count

}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")

    cell.textLabel.text = String(sectionInTable.objectAtIndex(indexPath.section).objectAtIndex(indexPath.row))        

    return cell
}

Sorry if I did mistakes, I'm not familiar with swift but I think the idea can help.

Softy answered 27/10, 2014 at 21:6 Comment(1)
where is the method for adding title in sectionMinter
C
0

I implemented generic algorithm to sort out any objects which can by identified by some day. I guess would be helpful in such cases:

protocol DayCategorizable {
    var identifierDate: Date { get }
}
extension Array where Element: DayCategorizable {

    var daySorted: [Date: [Element]] {
        var result: [Date: [Element]] = [:]
        let calendar = Calendar.current
        self.forEach { item in
            let i = calendar.startOfDay(for: item.identifierDate)
            if result.keys.contains(i) {
                result[i]?.append(item)
            } else {
                result[i] = [item]
            }
        }
        return result
    }
}
Circumvent answered 30/7, 2019 at 11:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.