I'm new to swift & Xcode (teaching myself via tutorials and stackoverflow). I've written some code that adds places to a list in a TableView and now I am trying to sort that list of places into sections.
Specifically, I have a ViewController where I input name, neighborhood and friend (all strings) and this adds a Place to the bottom of my TableView.
I want to group this list of places by neighborhood, and display all the places in the same neighborhood together in a section, using the neighborhood string as the section header.
I'm close, but I'm not indexing my sections correctly. indexPath.section I believe is what I'm missing..
So far I have this code in my TableViewController:
// MARK: Properties
var places = [Place]()
override func viewDidLoad() {
super.viewDidLoad()
// Use the edit button item provided by the table view controller.
navigationItem.leftBarButtonItem = editButtonItem()
// Load any saved places, otherwise load sample data.
if let savedPlaces = loadPlaces() {
places += savedPlaces
}
else {
// Load the sample data.
loadSamplePlaces()
}
// Sort by neighborhood
places.sortInPlace { (place1, place2) -> Bool in
return place1.neighborhood < place2.neighborhood
}
}
// MARK: Getting Count, Number and Name of Neighborhoods
func getCountForNeighborhood(neighborhood:String) -> Int {
return places.filter { (place) -> Bool in
return place.neighborhood == neighborhood
}.count
}
func getNumberOfNeighborhoods() -> Int {
var neighborhoodCount = 0
var neighborhoodPrev = "Not a real neighborhood"
for place in places {
if place.neighborhood != neighborhoodPrev {
neighborhoodCount += 1
}
neighborhoodPrev = place.neighborhood
}
return neighborhoodCount
}
func getNeighborhoodForSection(section:Int) -> String {
var previousNeighborhood:String = "Not a real neighborhood"
var currentIndex = -1
for place in places {
if(place.neighborhood != previousNeighborhood) {
currentIndex += 1
}
if(currentIndex == section){
return place.neighborhood
}
previousNeighborhood = place.neighborhood
}
return "Unknown"
}
func loadSamplePlaces() {
let place1 = Place(name: "Motorino", neighborhood: "East Village", friend: "Maggles")!
let place2 = Place(name: "Bar Primi", neighborhood: "Lower East Side", friend: "Em")!
let place3 = Place(name: "El Carino", neighborhood: "Williamsburg", friend: "Ruby")!
places += [place1, place2, place3]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
//alter this for To Try, Been To sections =2
return getNumberOfNeighborhoods()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
let neighborhood = getNeighborhoodForSection(section)
return getCountForNeighborhood(neighborhood)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "PlaceTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! PlaceTableViewCell
// Fetches the appropriate place for the data source layout.
let place = places[indexPath.row]
cell.placeLabel.text = place.name
cell.neighborhoodLabel.text = place.neighborhood
cell.friendLabel.text = place.friend
return cell
}
//Add section headers
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return getNeighborhoodForSection(section)
}
I'm fairly certain it's an issue with my cellForRowAtIndexPath method. I think I'm missing some code to properly index the data into sections...
Current build displays
You've made a mess with sections.
In method tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) you're taking data from the places array based on the row of the indexPath.
But you don't "talk" about sections at all. That's why you tell the data source delegate to take the data of those 2 place objects regardless of the section it's asking you.
You've made a data structure in a relational style like this:
neighbourhood1 - place1
neighbourhood1 - place2
neighbourhood2 - place3
neighbourhood2 - place4
neighbourhood3 - place5
neighbourhood3 - place6
And believe me, it's really hard to interface this data structure with the UITableView data source concept, cause it's really hard to determine the section index from the single data.
Try to arrange your data in a tree like this:
neighbourhood1:
- place1
- place2
neighbourhood2:
- place3
- place4
neighbourhood3:
- place5
- place6
In an array of arrays, and then it will be simple, cause the indexPath pair (section, row) will match the indexes of your places[section][row]:
neighbourhood1: (section 0)
- place1 (path 0.0)
- place2 (path 0.1)
neighbourhood2: (section 1)
- place3 (path 1.0)
- place4 (path 1.1)
neighbourhood3: (section 2)
- place5 (path 2.0)
- place6 (path 2.1)
I do concur with the other answer: this will be easier if your store your data in a two-dimensional array organized by neighborhood first, then place second, then access the data as:
let place = places[indexPath.section, indexPath.row];
However, to your specific questions and current code, I see two things here. First, your sample data has a single item per neighborhood, but your table is showing two rows per neighborhood. That indicates that your numberOfRowsInSection function is returning 2 when we expect it to be 1. Make sure your getNeighborhoodForSection is returning the right section. Then look at why getCountForNeighborhood isn't returning 1 for each section. (As an aside, you probably already know this, but getNeighborhoodForSection assumes your data is always sorted by neighborhood. If you insert a new neighborhood out of order, you'll run into problems).
Second, cellForRowAtIndexPath you always pull data by the indexPath.row number. When you enter this function for each neighborhood, the indexPath.row number will always restart at 0 for each section (neighborhood). And therefore you'll pull the 1st item out of places (Motorino) for the first row in every section. That's why you see the same list of places repeated in each section.
Once you have the right number of rows per neighborhood, In cellForRowAtIndexPath you need to:
Get the neighborhood for index.section using your getNeighborhoodForSection(index.section);
Search through places until you find that neighborhood and save the index of the starting point, which I'll call neighborHoodStartIndex
let place = places[neighborHoodStartIndex + indexPath.row]
For debugging purposes, I'd suggest making a different number of sample places in each neighborhood. It'll be a little easier to spot problem patterns when you expect different numbers of rows in each section.
Related
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()
}
I am currently trying to create a UITableView that loads data as a user scrolls through it.
Basically, I have a data source with a lot of records. Way more than is feasible to load all at once. So I am querying 50 records at a time.
The problem lies in the fact that the user will be able to jump to the middle of this list via letters at the right. For example, if they press 'M' I load 50 records starting at 'M' into the list. Scrolling down will navigate through the M's and eventually the N's, O's, etc.
Of course, appending data to the bottom of the list is common and I was able to do this easily. I am having trouble finding reference or if it is done at all of appending 50 records to the top of the list as a user scrolls up.
For example, if the user hits 'M'. they should be able to scroll up and start seeing L's. I can append data to the beginning of the list, but the problem lies in the continuation of the scrolling list.
As of now, I can not get it working without a jump of the list or a complete stop.
Can anyone point me to someone who has done this cleanly?
OK, so I figured out the answer to my own question. Sorry for the post but maybe it will help someone else because I failed to find an exact solution to my answer else where, at least when it came to Swift.
First I loaded 50 records that started with 'M'. The JSON data would return the row number of these records. I created Int variables 'begin' and 'end' which held on to the range of rows I had loaded into my array. I set the numberOfRowsInSection to the 'end' of my range. This number was significantly more than the amount of rows I had loaded in my array, so I had to finagle how rows were loaded based on that array. Simple code snippet:
var begin:Int?
var end:Int = 0
func onDataLoad() {
data = data + DataHandler.data!
begin = data[0].Row
end = begin! + 50
tableView.reloadData()
let count = tableView.indexPathsForVisibleRows!.count - 2
let indexPath = NSIndexPath(forRow: begin! + count, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: false)
loading = false
}
func onBackwardData(evt:Event) {
data = DataHandler.data! + data
begin = begin! - 50
tableView.reloadData()
loading = false
}
func onForwardData() {
end = end + 50
data = data + DataHandler.data!
tableView.reloadData()
loading = false
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return end
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("UserCell", forIndexPath: indexPath) as! PulseListTableViewCell
let cellrow = indexPath.row - begin!
if cellrow >= 0 {
cell.name.text = data[cellrow].Name
}
return cell
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
row = indexPath.row
let count = tableView.indexPathsForVisibleRows!.count + 5
if(indexPath.row >= end - 5 && !loading)
{
loading = true
DataHandler.getUsers(end)
}
else if(indexPath.row <= begin! + count && begin! > 0 && !loading)
{
loading = true
var offset = begin! - 50
if offset < 0 {
offset = 0
}
DataHandler.getUsers(offset)
}
}
This code is crude and has some hard coded things that would need to be adjusted to handle getting to the very beginning or end of the list but the idea is there. Sorry if this is a totally obvious solution/issue in the iOS world but I'm new and thought it might help.
This has been killing me for a few hours now. I have a UITableViewController that has multiple data sections. My data source is simply an Array.
The problem I'm running into is that each section is repeating data from the array starting from the first index instead of "slicing" it as I expect it should.
Simplified example:
let sections = ["Section A", "Section B"]
let counts = [3, 5]
let source = ["a","b",c","d","e","f","g","h"]
// Output in simulator:
# Section A
- a
- b
- c
# Section B
- a
- b
- c
- d
- e
- and so on...
I would expect that "Section B" would be the next 5 results starting at "d" and not restart from the first index.
The relevant code is pretty standard stuff:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sections.count // returns 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return counts[section] // returns correct data
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let data = source[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! MyTableViewCell
// some cell formatting, populate UILabels, etc
cell.testLabel.text = data["test"].string
return cell
}
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("Header") as! MyTableViewHeaderCell
headerCell.backgroundColor = UIColor.grayColor()
headerCell.testHeaderLabel.text = sections[section]
return headerCell
}
Initial searching of SO led me to believe it's a cell reuse issue but after overriding prepareForReuse in my cell class, I don't think thats it.
Expected Results
# Section A
- a
- b
- c
# Section B
- d
- e
- f
- g
- h
Like I said, I'm expecting that dividing the TableView data in to sections would keep a reference to the array pointer and continue where it left off instead of starting back at 0 for each section.
Any thoughts or suggestions?
indexPath.row always returns the row-number inside a section.
In your second section, you need to add the number of rows displayed in all sections before.
Change let data = source[indexPath.row] to something like this:
let data = source[indexPath.row+counts[0]]
If you add more sections, this will be a bit more complicated to calculate.
Other idea:
If it is possible, you could rearrange your array. You could make a two-dimensional array. The main array would include arrays with the data for each section.
To display it, you' need to use indexPath.section, too.
dataArray[indexPath.section][indexPath.row]
Using the idea of FelixSFD, but with a little logical modification, so you can work dynamically:
Change this:
let data = source[indexPath.row]
for this:
var countIndex = indexPath.row
for section in 0...indexPath.section {
countIndex += counts[section]
}
let data = source[countIndex]
Be careful with this approach because you may have some performance issues on large tableViews.
If you can rearrange your array:
change
let source = ["a","b",c","d","e","f","g","h"]
into
let source = [["a","b","c"],["d","e","f","g","h"]]
and change
let data = source[indexPath.row]
into
let data = source[indexPath.section][indexPath.row]
I had the same problem, but with more complex situation, and i needed more dynamically way of doing it. Sure i could rearrange my data, to use two-dimensional array, but i don't want to handle it later. So i did it like this.
I am pulling my data from firebase, so i never know, how many sections/arrays i will have.
Creating an array, to insert amount of items in array.
var counterTableView = [Int]()
Filling array with 0, without doing it, i was getting errors later. (Index out of range)
override func numberOfSections(in tableView: UITableView) -> Int {
for i in 0...Array(Set(self.sections)).count {
counterTableView.insert(0, at: i)
}
counterTableView.removeLast(counterTableView.count-Array(Set(self.sections)).count-1)
return Array(Set(self.sections)).count
}
Next step, is to fill the amount of items in one section in array
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
counterTableView[section+1] = counts[section] + counterTableView[section]}
Last step, showing the data in cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell.textLabel.text = source[indexPath.row+counterTableView[indexPath.section]]}
I am currently making an app using swift that has information about cars.
I am using an UITableView for makes, models, years.
What I want to know is if can I have an UITableView linked to another UITableView depending on user input, for example:
tableview 1 (makes)
Audi
Honda
tableview 2 (Models)
Audi -> A1, A2, A3........
Honda -> Civic, Jazz...
tableview 3 (years)
Audi -> A3 -> 2005,2006,2007.....
Honda -> Civic -> 2005,2006,2007.....
Code for tableview 1
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.Makes = [Make(name: "Audi"),Make(name: "Nissan"),Make(name: "Fiat"),Make(name: "Ford"),Make(name: "Honda"),Make(name: "Mercedes-Benz"),Make(name: "Lexus"),Make(name: "BMW"),Make(name: "Vauxhall"),Make(name: "VW")]
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.Makes.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
var make = Makes[indexPath.row]
cell.textLabel?.text = make.name
return cell
}
1.- Organise your data in a good manner, maybe a graph, a tree, or simply lists relating all of your data.
2.- For simplicity makes functions that will give you corresponding data to each tableview.
Lets say:
func getModels(make: Makes) -> [Model]
func getYears(model: Model) -> [Years]
or simply
func getModels(make: String) -> [String]
func getYears(model: String) -> [String]
also, some helper functions that will allow you to implement any data structure behind, just like, for example:
func getMaker(int:Int) -> Maker? or func getMaker(int: Int) -> String?
3.- You must keep in memory which of your possible makers and models have been selected, for now, keep it like:
var selectedMaker: String?
var selectedModel: String?
4.- I assume you will have all your UITableViews at the same UIViewController or UITableViewController, so you will need to decide corresponding data to show to every one.
For this you will need to differentiate each one, how is up to show, with tags, instance equality, etc. I suggest for later readability and facility of use to end up having a function that will return a number? maybe, corresponding to the tableview. For this explanation sake, lets call it func whichTableIsThis(tableView: UITableView) -> Int?
5.- Your delegates should work different for everyone of those tableviews. Here we will be using our brand new function that must return 1, 2 or 3 ..nil if this tableview is not one of those. :)
extension YourViewControlerWithTableViews: UITableViewDelegate, UITableViewDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//I'm assuming you will have only one cell, lets call it `AttributesTableViewCell`
var cell = tableView.dequeueReusableCellWithIdentifier("yourCellName", forIndexPath: indexPath) as! AttributesTableViewCell
cell.attributeValue.text = ""
if let tableNumber = whichTableIsThis(tableView) {
//here you will be checking for every of your tree cases, for this example I will check just for Models
//OK, so tableNumber returned 2
if tableNumber == 2 && selectedMaker != nil{
let value = getModels(selectedMaker!)[indexPath.row]
cell.attributeValue.text = value
}
//...
}
return cell
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let tableNumber = whichTableIsThis(tableView) {
//here you will be checking for every of your tree cases, for this example I will check just for Models
//OK, so tableNumber returned 2
if tableNumber == 2 && selectedMaker != nil{
return getModels(selectedMaker!).count
}
//...
}
return 0
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let tableNumber = whichTableIsThis(tableView) {
//here you will be checking for every of your tree cases, for this example I will check just for Maker
//OK, so tableNumber returned 1
if tableNumber == 1 {
selectedMaker = getMaker(indexPath.row)
//Here you must refresh data for your next tables in hierarchy, to allow them to refresh with new data
selectedModel = nil
selectedYear = nil
tableview2.reloadData()
tableview3.reloadData()
}
//...
}
}
}
And..that should be all. Hope it helps!
This approach of drilling down to see more details is very common, and Xcode even provides a template to illustrate this, called Master-Detail.
The way this works is when you select a row in the first (or Master) tableView, it performs a showDetail segue to the second (or detail) tableViewController.
In prepareForSegue, you would get the indexPath of the selected row, and pass the make to the detail (destination) view controller. That view controller would then show all the models for that make of car.
You would use the same process in the detail tableView to pass a specific model to the a third tableViewController to see all years for that make and model.
override func prepareForSegue(segue: UIStoryboardSegue,
sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
Let make = Makes[indexPath.row]
let controller = (segue.destinationViewController
as UINavigationController).topViewController
as! DetailViewController
controller.detailItem = make
controller.navigationItem.leftBarButtonItem =
splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
Update:
The Master-Detail template provides other benefits, such as Adaptive UI. For example, on an iPad or iPhone 6 Plus, the user could choose to see both the master and detail views in a split view.
At first the indexpath are called in sequence of 0,1,2... but after clicking on the table view , the func tablview is called with indexpath that seems completely random , and hence i am unable to reproduce the same data . I am using array to correlate the rows of the table .
The code is here:
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
return myList.count
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
// Configure the cell...
println("table view --> ")
let CellID : NSString = "Cell"
var cell : UITableViewCell = tableView?.dequeueReusableCellWithIdentifier(CellID)as UITableViewCell
var VehicleName = cell.viewWithTag(1) as UILabel
var VehicleNumber = cell.viewWithTag(2) as UILabel
if let ip = indexPath
{
VehicleName.text = arrayData[ip.row*2];
VehicleNumber.text = arrayData[ip.row*2+1];
}
}
return cell
}
You have a design problem. You need to be able to supply the data as requested in a random access fashion.
Study the documentation on UITableView and `UITableViewDataSource'.
The API calls cellForRowAtIndexPath as it needs the data, it just requests the data that it needs to display, not all the data each time. It only creates the cells that are being displayed. If you had 1000 rows of data and were displaying rows 900 through say 903 it only needs that data. If the view scrolls to display one more row it only needs more data from row 904.
There is absolutely no guarantee in what order of indexPaths cellForRowAtIndexPath gets called. It is upon the UIKit framework.
You need to, and can, fully control how your data source array holds data. In this case, it is arrayData - I also have a feeling that this is in some way related to myList, but it is not visible from your code snippet. Work on how elements are arranged within arrayData and then you will have expected elements in all cells.