Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make alphabetically section headers in table view with a mutable data source

I store strings of a view controller in a string array. I import this string array as a Data Source in my table view. This all works smoothly. But now I would like to sort the table view and add section headers. The section header should be from the alphabet, the rows of the meaning sections should be all strings from the array, starting with the letter of the section header.

I know how I can achieve this with static arrays. But how can I make it that only the sections are shown, which also have rows(strings in the array)? And how can I make it so that it generates a new section when saving a new string with a letter, which does not yet exist in the sections?

I hope I have explained it accurately enough. I tried for a long time to solve this problem. It would be great if someone could help me.

Here are some code snippets:

class OverViewController: UIViewController {

@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var addButton: UIBarButtonItem!
@IBOutlet weak var editButton: UINavigationItem!


var kontaktListe = Telefonbuch.loadArray()
var sections = [[String]]()
var collation = UILocalizedIndexedCollation.currentCollation()


override func viewDidLoad()
{
    super.viewDidLoad()
    tableView.dataSource = self
    configureSectionData()
    tableView.reloadData()

}


func configureSectionData()
{
    let names = kontaktListe.map{$0.name}

    let selector: Selector = "description"


    sections = Array(count:collation.sectionTitles.count, repeatedValue: [])

    let sortedObjects = collation.sortedArrayFromArray(names, collationStringSelector: selector)

    for object in sortedObjects {
        let sectionNumber = collation.sectionForObject(object, collationStringSelector: selector)
        sections[sectionNumber].append(object as! String)
    }
}

I load the object var kontaktListe = Telefonbuch.loadArray() and get the name property let names = kontaktListe.map{$0.name}. And there I would like to get the strings to sort and add from.

like image 821
Mikey Avatar asked Apr 09 '16 19:04

Mikey


3 Answers

I would change the way you store your contacts to a dictonary with the initial letters as keys and put the names that correspond to that initial letter into a subarray:

contacts = ["A": ["Anton", "Anna"], "C": ["Caesar"]]

I simplified the way of the contacts here (in form of strings), but you get the concept.

I would also save the section number of the letter in a seperate array like this:

letters = ["A", "C"]

Keep the array sorted and organized, so check after each insertion/deletion/update. This is not part of the table view implementation. I would make the Viewcontroller a delegate of the phonebook, so you can fire an update-like method from the phonebook to update the table.

How to get the data for the data source:

the number of sections:

letters.count

the section title for section at index i is

letters[i]

the number of cells in a section i is

contacts[letters[i]].count

and the content for a specific cell c in section i is:

contacts[letters[i]][c]

Feel free to ask further questions if anything is still not clear.

UPDATE - How to generate the arrays:

I don't require the data to be sorted, if you pass it already sorted, you can delete the sorting lines below ...

let data = ["Anton", "Anna", "John", "Caesar"] // Example data, use your phonebook data here.

// Build letters array:

var letters: [Character]

letters = data.map { (name) -> Character in
    return name[name.startIndex]
}

letters = letters.sort()

letters = letters.reduce([], combine: { (list, name) -> [Character] in
    if !list.contains(name) {
        return list + [name]
    }
    return list
})


// Build contacts array:

var contacts = [Character: [String]]()

for entry in data {

    if contacts[entry[entry.startIndex]] == nil {
        contacts[entry[entry.startIndex]] = [String]()
    }

    contacts[entry[entry.startIndex]]!.append(entry)

}

for (letter, list) in contacts {
    list.sort()
}

For Swift 3:

let data = ["Anton", "Anna", "John", "Caesar"] // Example data, use your phonebook data here.

// Build letters array:

var letters: [Character]

letters = data.map { (name) -> Character in
    return name[name.startIndex]
}

letters = letters.sorted()

letters = letters.reduce([], { (list, name) -> [Character] in
    if !list.contains(name) {
        return list + [name]
    }
    return list
})


// Build contacts array:

var contacts = [Character: [String]]()

for entry in data {

    if contacts[entry[entry.startIndex]] == nil {
        contacts[entry[entry.startIndex]] = [String]()
    }

    contacts[entry[entry.startIndex]]!.append(entry)

}

for (letter, list) in contacts {
    contacts[letter] = list.sorted()
}

I ran the code in playground and got the following outputs for

letters:

["A", "C", "J"]

contacts:

["J": ["John"], "C": ["Caesar"], "A": ["Anton", "Anna"]]
like image 160
Stefan Avatar answered Nov 07 '22 00:11

Stefan


For Swift 3. Thank you @Stefan! Here is my version with Set

var tableViewSource: [Character : [String]]!
var tableViewHeaders: [Character]!

let data = ["Anton", "Anna", "John", "Caesar"]

func createTableData(wordList: [String]) -> (firstSymbols: [Character], source: [Character : [String]]) {

    // Build Character Set
    var firstSymbols = Set<Character>()

    func getFirstSymbol(word: String) -> Character {
        return word[word.startIndex]
    }

    wordList.forEach {_ = firstSymbols.insert(getFirstSymbol(word: $0)) }

    // Build tableSourse array
    var tableViewSourse = [Character : [String]]()

    for symbol in firstSymbols {

        var words = [String]()

        for word in wordList {
            if symbol == getFirstSymbol(word: word) {
                words.append(word)
            }
        }

        tableViewSourse[symbol] = words.sorted(by: {$0 < $1})
    }

    let sortedSymbols = firstSymbols.sorted(by: {$0 < $1})

    return (sortedSymbols, tableViewSourse)
}

func getTableData(words: [String]) {
    tableViewSource = createTableData(wordList: words).source
    tableViewHeaders = createTableData(wordList: words).firstSymbols
}

getTableData(words: data)

print(tableViewSource)  // ["J": ["John"], "C": ["Caesar"], "A": ["Anna", "Anton"]]
print(tableViewHeaders) // ["A", "C", "J"]
like image 36
Włodzimierz Woźniak Avatar answered Nov 07 '22 00:11

Włodzimierz Woźniak


I did it within one loop, not few (Swift 4):

struct ContactData {
    let longName: String
    let phones: [String]
    let thumbnailImageData: Data?
}
var contacts = [ContactData]()
var tableViewSource = [Character : [ContactData]]()
var headerTitles = [Character]()

func createContactsData(completionHandler: @escaping () -> Swift.Void) {
    contacts = extractContacts() // convert CNContact to custom ContactData
    tableViewSource.removeAll()
    var prevChar: Character?
    var currentBatch: [ContactData]!
    contacts.forEach { contact in
        guard let firstChar = contact.longName.first else {
            return
        }
        if prevChar != firstChar {
            if prevChar != nil {
                tableViewSource[prevChar!] = currentBatch
            }
            prevChar = firstChar
            currentBatch = [ContactData]()
        }
        currentBatch.append(contact)
    }

    let allKeys = Array(tableViewSource.keys)
    let sortedSymbols = allKeys.sorted(by: {$0 < $1})
    headerTitles = sortedSymbols
    completionHandler()
}
like image 22
FreeGor Avatar answered Nov 07 '22 00:11

FreeGor