sectionForSectionIndexTitle retrieve previous section - ios

I have a UITableView with sectionIndexTitles. Here's my data source :
let sSectionTitles = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","#"]
var sectionTitles = [String]()
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
return sSectionTitles
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
var section = 0
if let selectedSection = sectionTitles.indexOf(title) {
section = selectedSection
} else {
section = index
}
return section
}
The variable sectionTitles is a similar array to sSectionTitles except that it only contains section indexes that are valid. For example, if I have no Contact with their name starting with the letter D, then "D" won't be in sectionTitles.
I'm trying to replicate the behavior in the Contact application :
If the user clicks on the Index title "D" and if there is at least one contact in the B section, then scroll to this section.
Else, scroll to the previous section available. (In this example, if there are no contacts for the B and C letter then scroll to A)
I've been stuck of this for many hours I still don't know how I could apply this logic. I thought about using a recursive function but I didn't manage to pull this off. Does someone has any lead on how this could be achieved?
Thanks.

I think you can do it by recursion. Use another helper function to retrieve the appropriate index and call it from tableview data source function. Example,
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
var section = getSectionIndex(title)
return section
}
//recursive function to get section index
func getSectionIndex(title: String) -> Int {
let tmpIndex = sectionTitles.indexOf(title)
let mainIndex = sSectionTitles.indexOf(title)
if mainIndex == 0 {
return 0
}
if tmpIndex == nil {
let newTitle = sSectionTitles[mainIndex!-1]
return getSectionIndex(newTitle)
}
return tmpIndex!
}

Related

Data not correctly getting populated in tableview sections

I am making an app in which I need this thing in one of the screens.
I have used the tableview with sections as shown in the code below
var sections = ["Adventure type"]
var categoriesList = [String]()
var items: [[String]] = []
override func viewDidLoad() {
super.viewDidLoad()
categoryTableView.delegate = self
categoryTableView.dataSource = self
Client.DataService?.getCategories(success: getCategorySuccess(list: ), error: getCategoryError(error: ))
}
func getCategorySuccess(list: [String])
{
categoriesList = list
let count = list.count
var prevInitial: Character? = nil
for categoryName in list {
let initial = categoryName.first
if initial != prevInitial { // We're starting a new letter
items.append([])
prevInitial = initial
}
items[items.endIndex - 1].append(categoryName)
}
for i in 0 ..< count
{
var tempItem = items[i]
let tempSubItem = tempItem[0]
let char = "\(tempSubItem.first)"
sections.append(char)
}
}
func getCategoryError(error: CError)
{
print(error.message)
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section]
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = categoryTableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
cell.textLabel?.text = self.items[indexPath.section][indexPath.row]
return cell
}
But it is producing runtime errors on return self.items[section].count
The reason for this error is because I am loading data (items array) is from server and then populating sections array after it. At the time when tableview gets generated, both the sections and items array is empty. That is why error occurs.
I am new to iOS and not getting grip over how to adjust data in sections of tableview.
Can someone suggest me a better way to do this?
What should be number of rows in section when I have no idea how much items server call will return?
Also i want to cover the case when server call fails and no data is returned. Would hiding the tableview (and showing error message) be enough?
Any help would be much appreciated.
See if this works: Make your data source an optional:
var items: [[String]]?
And instantiate it inside your getCategorySuccess and fill it with values. Afterwards call categoryTableView.reloadData() to reload your table view.
You can add a null check for your rows like this:
return self.items?[section].count ?? 0
This returns 0 as a default. Same goes for number of sections:
return self.items?.count ?? 0
In case the call fails I would show an error message using UIAlertController.
Your comment is incorrect: "At the time when tableview gets generated, both the sections and items array is empty. That is why error occurs."
According to your code, sections is initialized with one entry:
var sections = ["Adventure type"]
This is why your app crashes. You tell the tableview you have one section, but when it tries to find the items for that section, it crashes because items is empty.
Try initializing sections to an empty array:
var sections = [String]()
Already things should be better. Your app should not crash, although your table will be empty.
Now, at the end of getCategorySuccess, you need to reload your table to reflect the data retrieved by your service. Presumably, this is an async callback, so you will need to dispatch to the main queue to do so. This should work:
DispatchQueue.main.async {
self.categoryTableView.reloadData()
}

Indexing error with an array in a dictionary (Swift 3.0)

I am having an issue displaying indexed objects in a table view. I have indexed the objects as a list of clients based on the first letter of their name. The indexed results are put into a dictionary like this
{Char: [Client]}
Char being the first character of the client's name and [Client] being an array of client objects, who have the first letter of their name matching the Char. When printed out, this shows up like this:
{'D': [Client("David"), Client("Dan")]}
However, when I go to set these titles in the tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) function, I get an index out of bounds error. The client array has a count of 3 and the contacts dictionary has a count of two, but has all 3 objects it as such:
{'S': [Client("Sam")], 'D': [Client("David"), Client("Dan")]}
How do I go through the dictionary properly to get individual clients into each table cell with the proper section headers? I am using Swift 3.0. Below I have posted how I indexed it as well as the function I am trying to override.
ClientTableViewController.swift
var clients = Client.loadAllClients()
var contacts = [Character: [Client]]()
var letters: [Character] = []
override func viewDidLoad() {
super.viewDidLoad()
letters = clients.map { (name) -> Character in
return name.lName[name.lName.startIndex]
}
letters = letters.reduce([], { (list, name) -> [Character] in
if !list.contains(name) {
return list + [name]
}
return list
})
for c in clients {
if contacts[c.lName[c.lName.startIndex]] == nil {
contacts[c.lName[c.lName.startIndex]] = [Client]()
}
contacts[c.lName[c.lName.startIndex]]!.append(c)
}
for (letter, list) in contacts {
contacts[letter] = list.sorted(by: { (client1, client2) -> Bool in
client1.lName < client2.lName
})
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Name", for: indexPath) as! ClientTableViewCell
cell.clientName?.text = contacts[letters[indexPath.row]]?[indexPath.row].lName
return cell
}
Create one section per letter, by implementing numberOfSections method to return this:
return letters.count
Change code in cellForRowAt from this:
cell.clientName?.text = contacts[letters[indexPath.row]]?[indexPath.row].lName
To this:
cell.clientName?.text = contacts[letters[indexPath.section]]?[indexPath.row].lName

UITableView SectionIndexTitle Custom View

I've googled for about 2 days about my problem already. But I couldn't find any solution that matches with my problem. My problem is I want to make a custom SectionIndexTitle for UITableView like an image below. Please recommend me any library or concept to do it. Thank you.
If you're trying to create an alphabetic index there is method in UITableViewDelegate that supports it out of the box.
You'll need to define your sections and then use UILocalized​Indexed​Collation to index them.
let collation = UILocalizedIndexedCollation.currentCollation()
as UILocalizedIndexedCollation
// table sections
var sections: [Section] {
if self._sections != nil {
return self._sections!
}
// create objects from your datasource
var objects: [Object] = names.map { objectAttribute in
var object = Object(objectAttribute: objectAttribute)
object.section = self.collation.sectionForObject(object, collationStringSelector: objectAttribute)
return object
}
// create empty sections
var sections = [Section]()
for i in 0..<self.collation.sectionIndexTitles.count {
sections.append(Section())
}
override func sectionIndexTitlesForTableView(tableView: UITableView)
-> [AnyObject] {
return self.collation.sectionIndexTitles
}
override func tableView(tableView: UITableView,
sectionForSectionIndexTitle title: String,
atIndex index: Int)
-> Int {
return self.collation.sectionForSectionIndexTitleAtIndex(index)
}
To read more about UILocalized​Indexed​Collation

Get Album titles for indexed uitableview using MPMediaQuery in swift?

I'm finding the deeply nested structure of the MPMediaQuery difficult to navigate.
I'm trying to get the title(s) of albums for each Section to display in an indexed UITableView.
The basic query and code to get all albums:
let myQuery:MPMediaQuery = MPMediaQuery.albumsQuery()
myQuery.groupingType = MPMediaGrouping.Album
let allAlbums = myQuery.collections
// This prints the total number of albums (either way works)
// Or so I thought - but this does not give the album count per section
// I don't know what this is returning!
print("number albums \(myQuery.collections?.count)")
print("number albums \(allAlbums?.count)")
// this prints out the title of each album
for album in allAlbums!{
print("---------------")
print("albumTitle \(album.representativeItem?.albumTitle)")
print("albumTitle \(album.representativeItem?.valueForProperty(MPMediaItemPropertyAlbumTitle))")
}
This handles the TableView Index stuff:
// create index title
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
let sectionIndexTitles = myQuery.itemSections!.map { $0.title }
return sectionIndexTitles
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
return index
}
// tableview
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return (myQuery.itemSections![section].title)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// print("number of sections \(myQuery.itemSections?.count)")
return (myQuery.itemSections?.count)!
}
I'm at a loss to determine how to print out the Album titles for each section (where a section is "A", "B", etc) like:
A
Abbey Road
Achtung Baby
All The Young Dudes
B
Baby The Stars Shine Bright
C
Cosmic Thing
etc.....
I'm going to answer my own question.
I've posted similar questions here on SO and everyone replying said this couldn't be done and that additional Arrays were needed plus custom sort routines. That's simply not true.
This code uses the return from an Album Query to:
1) Build a TableView index
2) Add Albums by Title (using Apple's sorting) to table Sections
3) Start playing an album when selected
Here's the Swift code to prove it:
// Set up a basic Albums query
let qryAlbums = MPMediaQuery.albumsQuery()
qryAlbums.groupingType = MPMediaGrouping.Album
// This chunk handles the TableView Index
//create index title
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
let sectionIndexTitles = qryAlbums.itemSections!.map { $0.title }
return sectionIndexTitles
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
return index
}
// This chunk sets up the table Sections and Headers
//tableview
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return (qryAlbums.itemSections![section].title)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return (qryAlbums.itemSections?.count)!
}
// Get the number of rows per Section - YES SECTIONS EXIST WITHIN QUERIES
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return qryAlbums.collectionSections![section].range.length
}
// Set the cell in the table
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
// i'm only posting the pertinent Album code here.
// you'll need to set the cell details yourself.
let currLoc = qryAlbums.collectionSections![indexPath.section].range.location
let rowItem = qryAlbums.collections![indexPath.row + currLoc]
//Main text is Album name
cell.textLabel!.text = rowItem.items[0].albumTitle
// Detail text is Album artist
cell.detailTextLabel!.text = rowItem.items[0].albumArtist!
// Or number of songs from the current album if you prefer
//cell.detailTextLabel!.text = String(rowItem.items.count) + " songs"
// Add the album artwork
var artWork = rowItem.representativeItem?.artwork
let tableImageSize = CGSize(width: 10, height: 10) //doesn't matter - gets resized below
let cellImg: UIImageView = UIImageView(frame: CGRectMake(0, 5, myRowHeight-10, myRowHeight-10))
cellImg.image = artWork?.imageWithSize(tableImageSize)
cell.addSubview(cellImg)
return cell
}
// When a user selects a table row, start playing the album
// This assumes the myMP has been properly declared as a MediaPlayer
// elsewhere in code !!!
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let currLoc = qryAlbums.collectionSections![indexPath.section].range.location
myMP.setQueueWithItemCollection(qryAlbums.collections![indexPath.row + currLoc])
myMP.play()
}
Also, here's a few helpful notes:
1) List all the Albums from the query:
for album in allCollections!{
print("---------------")
print("albumTitle \(album.items[0].albumTitle)")
print("albumTitle \(album.representativeItem?.albumTitle)")
print("albumTitle \(album.representativeItem?.valueForProperty(MPMediaItemPropertyAlbumTitle))")
} // each print statement is another way to get the title
2) Print out part of the query to see how it's constructed:
print("xxxx \(qryAlbums.collectionSections)")
I hope this helps some of you - if so up vote!

How to add list of songs with sections to UITableView using Swift?

I'm trying to create a simple music player that lists all the songs on my device in a UITableView split into alphabetic sections (like the Apple Music Player).
There are two areas I can't figure out:
Getting the number of songs per section so that I can create the
correct number of rows in my table for each section
Accessing the items in each section to populate the section cells
My code is already pretty long so I'll put the pertinent info here:
I get the list of songs like this:
let songsQry:MPMediaQuery = MPMediaQuery.songsQuery()
This is where I'm unsure:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//How can I get the number of songs per Section to return??
// this does not work:
return songsQry.itemSections![0].count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
//How do I get the song for the Section and Row??
cell.textLabel?.text = ????
return cell
}
I'm going to answer my own question.
I've posted similar questions here on SO and everyone replying said this couldn't be done and that additional Arrays were needed plus custom sort routines. That's simply not true.
Note however, that in my example code I show how get the same kind of results for an Album Query. The code for an Artist query is almost identical.
This code uses the return from an Album Query to:
1) Build a TableView index
2) Add Albums by Title (using Apple's sorting) to table Sections
3) Start playing an album when selected
Here's the Swift code to prove it:
// Set up a basic Albums query
let qryAlbums = MPMediaQuery.albumsQuery()
qryAlbums.groupingType = MPMediaGrouping.Album
// This chunk handles the TableView Index
//create index title
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
let sectionIndexTitles = qryAlbums.itemSections!.map { $0.title }
return sectionIndexTitles
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
return index
}
// This chunk sets up the table Sections and Headers
//tableview
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return (qryAlbums.itemSections![section].title)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return (qryAlbums.itemSections?.count)!
}
// Get the number of rows per Section - YES SECTIONS EXIST WITHIN QUERIES
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return qryAlbums.collectionSections![section].range.length
}
// Set the cell in the table
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
// i'm only posting the pertinent Album code here.
// you'll need to set the cell details yourself.
let currLoc = qryAlbums.collectionSections![indexPath.section].range.location
let rowItem = qryAlbums.collections![indexPath.row + currLoc]
//Main text is Album name
cell.textLabel!.text = rowItem.items[0].albumTitle
// Detail text is Album artist
cell.detailTextLabel!.text = rowItem.items[0].albumArtist!
// Or number of songs from the current album if you prefer
//cell.detailTextLabel!.text = String(rowItem.items.count) + " songs"
// Add the album artwork
var artWork = rowItem.representativeItem?.artwork
let tableImageSize = CGSize(width: 10, height: 10) //doesn't matter - gets resized below
let cellImg: UIImageView = UIImageView(frame: CGRectMake(0, 5, myRowHeight-10, myRowHeight-10))
cellImg.image = artWork?.imageWithSize(tableImageSize)
cell.addSubview(cellImg)
return cell
}
// When a user selects a table row, start playing the album
// This assumes the myMP has been properly declared as a MediaPlayer
// elsewhere in code !!!
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let currLoc = qryAlbums.collectionSections![indexPath.section].range.location
myMP.setQueueWithItemCollection(qryAlbums.collections![indexPath.row + currLoc])
myMP.play()
}
Also, here's a few helpful notes:
1) List all the Albums from the query:
for album in allCollections!{
print("---------------")
print("albumTitle \(album.items[0].albumTitle)")
print("albumTitle \(album.representativeItem?.albumTitle)")
print("albumTitle \(album.representativeItem?.valueForProperty(MPMediaItemPropertyAlbumTitle))")
} // each print statement is another way to get the title
2) Print out part of the query to see how it's constructed:
print("xxxx \(qryAlbums.collectionSections)")
I hope this helps some of you - if so up vote!
The idea is simple:
You need to create a sorted array GroupedSongs which holds objects
of type [String: [MPMediaItem]] (key is the character and
the array holds the songs corresponding that character). The
[MPMediaItem] array must be sorted from the title property.
Setting up the Table View Datasource
For each key we need a section which now is the main array
For each item we need a number a songs, so this will be the number
of rows for our tableView
Implementation
Code has comments so no more talking :D
var items: [[String: [MPMediaItem]]] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
items = loadSongsDividedByTitle()
}
// MARK: - Delegation
// MARK: Table view datasource
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let object: [String: [MPMediaItem]] = items[section]
let key = Array(object.keys)[0] // We always have one object there
let songs: [MPMediaItem] = object[key]!
return songs.count
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
// Load song from the array
let object: [String: [MPMediaItem]] = items[indexPath.section]
let key = Array(object.keys)[0] // We always have one object there
let songs: [MPMediaItem] = object[key]!
let song = songs[indexPath.row]
// Bind title of the song to the cell text label
cell.textLabel?.text = song.title
return cell
}
// MARK: - Helpers
func loadSongsDividedByTitle() -> [[String: [MPMediaItem]]] {
guard var songs = MPMediaQuery.songsQuery().items else { return [] }
// Sort songs
songs = songs.sort({$0.title < $1.title})
// Create a new dictionary to hold array of words for each letter
var object: [String: [MPMediaItem]] = [:]
// Songs holding a dictionary with music
var groupedSongs : [[String: [MPMediaItem]]] = []
// Enumerate in words
for mediaItem in songs {
guard let title: String = mediaItem.title! else { continue } // If we don't have the title skip the song
// Use the first character as a key for each array
let key = String(title.characters.first!)
if var songGroup = object[key] {
// If we have an array, then append the new word there
songGroup.append(mediaItem)
object[key] = songGroup
} else {
// Create an array for that key
object[key] = [mediaItem]
}
}
// Add every object into one big array
for (key, value) in object {
let sortedSongs = value.sort({$0.title < $1.title})
groupedSongs.append([key: sortedSongs])
}
// Sort it alphabetically from a-z
groupedSongs = groupedSongs.sort({Array($0.keys)[0] < Array($1.keys)[0]})
return groupedSongs
}
Playground output with strings

Resources