7

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.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Mikey
  • 131
  • 1
  • 2
  • 10
  • Seems like you'll just have to check your data source whether it contains the specific letter. Please add some of your code and show us what you've tried so far though, so we can help better. – Eendje Apr 09 '16 at 19:42
  • I added some code snippets. Does this help in any way? – Mikey Apr 09 '16 at 20:13

3 Answers3

15

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"]]
Amr Hossam
  • 2,313
  • 1
  • 22
  • 23
Stefan
  • 418
  • 5
  • 13
  • That's already a great answer, thanks a lot. There is still a question for me. How do I get the data from the string array (my data) into the letters array and the contacts dictionary? Do you have any advice for this? – Mikey Apr 10 '16 at 07:08
  • I added a possible solution to my answer. – Stefan Apr 10 '16 at 13:56
  • You are amazing, that is what I was looking for since weeks. Thank you so much for your help. – Mikey Apr 10 '16 at 16:41
  • I have one more question: I get an error like "fatal error: Can't form a Character from an empty String". It is in the following line: letters = data.map { (name) -> Character in return name[name.startIndex]} I think it happens, when there's no data in the data array. how can I change this code to allow also an empty array as data? – Mikey Apr 14 '16 at 22:07
  • In my Code Swift automatically derived the type of the array as String. In you pass an empty array, swift is not able to do that (how should it know ;) ). Just declare the type: let data: [String] This allows for example: let data: [String] = [] – Stefan Apr 15 '16 at 22:59
  • You are great Stefan, thanks so much and sorry for bothering again:-) – Mikey Apr 16 '16 at 08:32
  • Thank you! No problem :) – Stefan Apr 20 '16 at 14:34
  • I have a further question about that topic. I I would like to delete a row in that tableView. How would I use commitEditingStyle: for deleting the cell. Do I have to delete the entry in dataSource (contacts) or in the dictionary? And how would i do that? – Mikey Aug 19 '16 at 08:42
  • I get an error on this part `contacts[entry[entry.startIndex]]` maybe you could take a look at http://stackoverflow.com/questions/41535540/alphabetic-index-with-mpmediaquery for my specific problem it would be really appreciated – NoSixties Jan 08 '17 at 22:51
  • @Mikey: Take a look on this page: https://developer.apple.com/reference/uikit/uitableviewdatasource/1614871-tableview?language=objc It explains inserting and deleting pretty well – Stefan Jan 10 '17 at 14:47
7

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"]
Włodzimierz Woźniak
  • 3,106
  • 1
  • 26
  • 23
4

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()
}
FreeGor
  • 615
  • 13
  • 26
  • Can you add more info like, for example, what is currentBatch and what is tableViewSource, Dictionary or Array? Please provide more info – Boris Nikolic Jan 30 '18 at 14:02
  • @BorisNikolić see code please, it shows the type: var tableViewSource = [Character : [ContactData]](), so it's a Dictionary – FreeGor Jan 30 '18 at 15:51