netshark1000 netshark1000 - 17 days ago 5
Swift Question

Swift: Create Section index for list of names

I have written an algorithm to create a section index for a tableview.
Unfortunately I have a bug when the list contains only one item the result is empty.

Do you have an elegant solution for that?

var sections : [(index: Int, length :Int, title: String)] = Array()

func createSectionIndices(participants: List<Participant>){

sections.removeAll()

var index = 0;

let array = participants.sort({$0.lastName < $1.lastName})

for i in 0.stride(to: array.count, by: 1){

let commonPrefix = array[i].lastName.commonPrefixWithString(array[index].lastName, options: .CaseInsensitiveSearch)

if (commonPrefix.isEmpty) {

let string = array[index].lastName.uppercaseString;
let firstCharacter = string[string.startIndex]
let title = "\(firstCharacter)"
let newSection = (index: index, length: i - index, title: title)
sections.append(newSection)
index = i;
}
}
print("sectionCount: \(sections.count)")
}

Answer

Here's a one line solution to build the sections list:

 var participants:[(firstName:String, lastName:String)] = 
     [
        ("John", "Smith"),
        ("Paul", "smith"),
        ("Jane", "Doe"),
        ("John", "denver"),
        ("Leo",  "Twain"),
        ("Jude", "law")
     ]   

 // case insensitive sort (will keep "denver" and "Doe" together)
 participants = participants.sort({$0.lastName.uppercaseString < $1.lastName.uppercaseString})

 // The process:
 // - get first letter of each name (in uppercase)
 // - combine with indices (enumerate)
 // - only keep first occurrence of each letter (with corresponding indice)
 // - build section tuples using index, letter and number of participants with name begining with letter
 let sections = participants
                .map({String($0.lastName.uppercaseString.characters.first!)})
                .enumerate()
                .filter({ $0 == 0 || !participants[$0 - 1].lastName.uppercaseString.hasPrefix($1) })
                .map({ (start,letter) in return 
                       ( 
                         index:  start, 
                         length: participants.filter({$0.lastName.uppercaseString.hasPrefix(letter)}).count,
                         title:  letter
                       )
                    })

 // sections will contain:
 // (index:0, length:2, title:"D")
 // (index:2, length:1, title:"L")
 // (index:3, length:2, title:"S")
 // (index:5, length:1, title:"T")

You may already have a lot of existing code based on the sections being stored in an array of tuples but, if not, I would suggest you approach this a little differently and build your sections array with the letter AND the participant data.

 let sections = participants
                .map({ String($0.lastName.uppercaseString.characters.first!) })
                .reduce( Array<String>(), combine: { $0.contains($1) ? $0 : $0 + [$1] })      
                .map({ (letter) in return 
                       ( 
                         title: letter, 
                         participants: participants.filter({$0.lastName.uppercaseString.hasPrefix(letter)})
                       ) 
                    })

This would allow you to respond to the number of sections with sections.count but will also make it easier to manipulate index paths and data within each section:

  • number of participants in a section : sections[index].participants.count
  • participant at index path : sections[indexPath.section].participants[indexPath.row]

This is just syntactic candy but if you have a lot of references to the participants list, it will make the code more readable.
Also, if your participants are objects rather than tuples or structs, you can even update the data in you main participant list without having to rebuild the sections (unless a last name is changed).

[EDIT] fixed errors in last tuple syntax

Comments