My goal is to insert a section upon a click and insert 4 rows into this section.
I'm already familiar with how to use InsertRowsAtIndexPaths and inserting rows into one section makes no problem.But when it comes to inserting new sections, it's tricky and the apple documentation doesn't explain it fully.
Here is the code i use for inserting the rows
self.tableView!.beginUpdates()
var insertedIndexPaths: NSMutableArray = []
for var i = 0; i < newObjects.count + 1; ++i {
insertedIndexPaths.addObject(NSIndexPath(forRow: initialCount + i, inSection: sectionsnumbers)) }
self.tableView?.insertRowsAtIndexPaths(insertedIndexPaths as [AnyObject], withRowAnimation: .Fade)
self.tableView!.endUpdates()
Any examples, insights are very appreciated.
Thank you.
When you insert a section, its data is reloaded. You don't need to tell the tableview about all the new rows individually; it will just ask the data source for the number of rows in the new section.
See this document for more information.
use var sectionNumbers:NSMutableArray = ({"object":"object"}) //etc whatever object you want
Now use this function
func numberOfSectionsInTableView(tableView: UITableView) -> Int{
return sectionNumbers.count}
and add object in sectionNumbers and callThis method method where you want to add section
tableView.reloadData()
It will help
Inserting sections is different than inserting rows. If you want to insert rows you need to call tableView.beginUpdates() first and table.endUpdates() after done, otherwise it will throw some data inconsistency exception. On the other hand, when inserting sections you don't have to do anything, just reflect the change in your datasource like numberOfSectionsInTableView.
Related
The main idea is to have all sections in an array or you may suggest other solution to build a table. I had this code below to prepare data for a table:
enum ExerciseSection {
case empty
case exerciseGroup(group: ExerciseGroup)
}
struct ExerciseGroup {
var muscleGroupName: String
var exercises: [ExerciseEntity]
var selectedExercisesIndexes: [Int]
}
As you see using this ExerciseSection enum I can simple check if the section is static empty or it should display some muscle group name. Also Group contains exercises. So I can simple a build needed cell.
So I prepare data for my table by creating an array of ExerciseSection.
In this concert example my empty cell is a cell which redirect me to the other screen.
Looks like this:
[empty cell for section 0, group for section 1, group for section 2... and etc]
Now I changed mind of preparing my own sections and instead I started using CoreStore.monitorSectionedList
typealias ListEntityType = ExerciseEntity
let monitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
)
So now my data is grouped automatically by relationships muscle name.
I can simple access instance of monitor and see how many sections it has and how many rows it has for appropriate section. Pretty awesome!
But my question now how can I combine monitor object which has all needed info about grouped objects and about groups with my static cells.
In my example above I have the firs element empty cell for section 0 but monitor already has section 0 as well.
So I need to have a hack to add 1 + which I really don't like as this is a magical number and some day it will surprise me.
func numberOfSections(in tableView: UITableView) -> Int {
return 1 + (monitor.numberOfSections() ?? 0) // My static section + monitor sections
}
In the previous time I just had array of all my sections [ExerciseSection] so there is no needs to control code via 1 +
I need to glue somehow my static section info and monitor.sections
You may never used CoreStore before, so never mind you can just think about monitor object as an object that has some groups to represent sections and these groups has items to represent rows. So I just need to combine it.
In my case you can simple see that the static cell is a first one item in the list but I am looking for flexible solution I even can't imagine how to show static cell at the middle of list for example.
Maybe as a solution I can loop through monitor objects and create my enum from it. Not sure.
Hmm... The "easiest" way would be to have a computed property sections or similar.
Something along the lines of
var sections: [ExerciseSection] {
return [.empty] + monitor.sections
}
But if the monitor doesn't have a direct way to get sections, then maybe the best way would be to simply have a list of "pre-sections".
let presections: [ExerciseSection] = [.empty]
func numberOfSections(in tableView: UITableView) -> Int {
return presections.count + (monitor.numberOfSections() ?? 0) // My static section + monitor sections
}
You could add a couple of functions to help you such as
func section(at indexPath: IndexPath) -> ExerciseSection {
guard indexPath.section >= presections.count else {
return presections[indexPath.section]
}
return monitor.section(at: indexPath.section - presections.count)
}
You do mention looping through the monitor objects, and this can be nice for smaller datasets. The downside is that you suddenly store data in memory. I don't know how the monitor works.
With Realm I've done this but only stored the id and maybe some simple data for each row.
Background
In my app, I store a bunch of object IDs. I use these IDs to make batch API calls. The API limits each call to 10 ID numbers. This data is rendered on a UITableView. The user can add and delete objects, which adds or removes the object ID from the database.
I’m using a Firestore database to store the object IDs on my end.
Current Implementation
Here’s what I’ve implemented so far, but it crashes the app when add & deleting objects. I haven’t been able to work out how to properly handle these cases & whether this is the right pattern to do something like this.
Get object IDs to be used for making API calls
var objectIds: [String] = []
var chunkedObjectIds: [[String]] = []
var objects: [Array] = []
var offset: Int = 0
override func viewDidLoad() {
super.viewDidload()
getObjectIds()
}
func getObjectIds() {
// get objects IDs and store then in objectIds from the Firestore database
// setup the .addSnapshotLister so the query is triggered whenever there is a change in the data on Firestore for the collection
return chunkedObjectIds
// when finished, get the first 10 objects from the 3rd party API
fetchObjects()
}
Take object Ids array, split into array of arrays (lots of 10) & Make the API call for the first 10
func fetchObjects() {
// split objectIds array in array of arrays, in lots of 10
// chunkedObjectIds is set here
// request the objects for the first 10 ID numbers
Alamofire.request(… parameter with first 10 object ids …) (objects) in {
// save objects
// increment the offset
offset += 1
}
}
Render the data on the UITableView cells
Use the following method to load more data from the 3rd party API:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastRow = objects.count
var parameters = [String: Any]()
if indexPath.row == lastRow {
if !(offset == self.chunkedObjectIds.count) {
// process the next batch from the array
parameters["id-numbers"] = self.chunkedObjectIds[offset].map{String($0)}.joined(separator: ",")
Alamofire.request(… paramaters: parameters) { (objects) in
for item in 0..<objects.count {
let indexPath = IndexPath(row: item + self.objects.count, section: 0)
self.paths.append(indexPath)
}
self.objects.append(contentsOf: objects)
self.tableView.beginUpdates()
self.tableView.insertRows(at: self.paths, with: .automatic)
self.tableView.endUpdates()
self.paths.removeAll()
self.offset += 1
}
}
}
}
Adding or deleting objects:
The object ID is added or deleted from the Firestore database
The objectIds, chunkedObjectIds, offset and objects are cleared
The listener triggers a read of the data and the process repeats
The Issue & Question
This works well to load initial data. But duplication occurs when adding (and sometimes crashing). When deleting the app will crash because of out of range exceptions.
Is this the correct pattern to use in the first place? If so, what am I missing to handle cases after the first load, specifically the addition and deletion of new object IDs.
Edit
I have changed the implementation based on feedback in the comments. So now, the process is like this:
Setup listener to get data from Firestore
Loop through the object ids from Firestore and while the counter is < 10 or we reach object.count - Now I save the next offset and the next time it triggers this method, I initiate a loop from the next offset with the same while conditions
Fetch the objects from the 3rd party API
I kept using willDisplay cell method to trigger more data to load - it seemed to work more reliably than scrollDidEnd method.
So now the app doesn't crash anymore. There are some issues with the firestore listener, but I'll post that as a separate question.
I am writing a function to loop through all of the cells in a UITableViewController. Here is what I have so far:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
let cell = tableView.cellForRow(at: i-1)
}
Everything loops correctly until I try to get the cell. It expects an input of type IndexPath, however I am passing in an Int. Whenever I force it as an IndexPath like so:
let cell = tableView.cellForRow(at: i-1 as! IndexPath)
I get a warning saying that it will always fail/ always return nil. Is there a better way to do this, or am I just missing a crucial step. Any help is greatly appreciated. Thanks in advance!
EDIT (A LITTLE MORE EXPLANATION): All the cells are custom classed cells, with a specific variable. I want to loop through all of the cells and get that value.
let visibleCells = tableView.visibleCells
for aCell in visibleCells {
print(aCell.question?.text) <------- this is the value I want
}
Most of the information below has been said by others in the other answers and comments, but I wanted to put it all in one place:
We need to step back from the specifics of the question and ask what you are actually trying to do.
The UITableView method cellForRow(at:) will only return cells that are actually on-screen. If there is only room for 5 cells and you have to scroll to expose the rest, that method will return nil for all but the cells that are visible.
As others have suggested, if your goal is to loop through the cells that are on-screen the property tableView.visibleCells would be a better choice.
If your goal is to loop through all cells that exist in your data then you need to explain what you are trying to do.
As for your specific question, the cellForRow(at:) wants a parameter of type IndexPath. You can't simply cast an Int to IndexPath. That will fail. Instead, as #TaylorM said in their answer, you need to create an IndexPath. If your table view only has a single section then you can simply use
let indexPath = IndexPath(row: i, section: 0)
(Assuming you fix your loop code so your indexes start at 0.)
It also does not make sense to use a while loop like that.
Instead of all of this, I suggest using:
let visibleCells = tableView.visibleCells
for aCell in visibleCells {
//Do something with the cell
}
You should create an IndexPath via code, like this:
let ndx = IndexPath(row:i, section: 0)
Or, to modify your code:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
let ndx = IndexPath(row:i-1, section: 0)
let cell = tableView.cellForRow(at:ndx)
}
Based on the later edit where you mention that you want the value of a text string on each row, I would suggest that the above is probably not the best way to approach this :) (I know others have already said this, but I didn't want to make assumptions about what you wanted to do unless you specifically stated what you wanted ...)
You are probably better off taking the same approach you take to populate the data for the table view cells via cellForRowAt: to get the question text than to loop through all the table rows, which would result in some issues for non-visible rows as others have indicated already.
If you have any issues with getting the data provided for cellForRowAt:, do share the code for the cellForRowAt: with us and I'm sure one of us can help you figure things out :)
Try
let indexPath = IndexPath(row: i, section: 0)
You may need to adjust the section to fit your needs.
Check out the documentation for IndexPath. Like most classes and structures, IndexPath has initializers. If you need to create a new instance of any object, you'll most like use an initializer.
*also see the comment from #rmaddy:
"Unrelated to your question but I can almost guarantee that your attempt to loop through all of the cells is the wrong thing to do. What is your actual goal with that loop?"
I believe I have figured it out using all of the help from you wonderful people.
In my case, I have a custom class with a variable that I want access to. I can do what Taylor M, Fahim, and Duncan C said: let indexPath = IndexPath(row: i, section: 0)
Then I can add as! NumberTableViewCell to the end, which allows me access to the values defined there. In the end, it looked like this:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
var cell = tableView.cellForRow(at: IndexPath(row: i-1, section: 0)) as! NumberTableViewCell
print(cell.question.text!)
}
Thank you all for your help!
Everybody knows about the indexPathsForSelectedRows - but here come the time that i need to know the rows that are not selected
I would like to make an extension of it.
Is there any chance somebody already have done it or have an idea how this can be done?
If there is only one section in the table view you could simply
Map all row indexes from indexPathsForSelectedRowsto an Int array.
let rowIndexes = indexPathsForSelectedRows.map { $0.row }
Create an Int array from the indexes in the dataSourceArray
let allIndexes = 0..<dataSourceArray.count
Filter the indexes which are not in allIndexes and create new index paths
let indexPathsForDeselectedRows = allIndexes.filter {!rowIndexes.contains($0) }.map {NSIndexPath(forRow: $0, inSection: 0)}
If there are multiple sections it's a bit more complex.
I want to do a batch insertion into UITableview.
MyArray.all() do |datas|
self.tableView.beginUpdates
self.tableView.insertRowsAtIndexPaths(datas, withRowAnimation: UITableViewRowAnimationFade)
self.tableView.endUpdates
self.myarrayvar += datas
end
'myarrayvar' is an array of MyArray. I tried to refer to
apple documentation
and stackoverflow which is similar to my question.
But I really no idea how to convert my datas, to fit into the parameter of insertRowsAtIndexPaths function.
Using rubymotion
Here's one way that I'm currently using:
def insertCellsAtBottom(newData)
# Get count for the existing data you have.
currentCount = #data.count
# Build the indexPaths for the new data, adding to the current count each time.
# This will make sure you are creating the correct indexes.
indexPaths = newData.map.with_index do |obj, index|
NSIndexPath.indexPathForRow(currentCount + index, inSection: 0)
end
# Update your datasource and insert the rows.
self.tableView.beginUpdates
#data.addObjectsFromArray(newData)
self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimationFade)
self.tableView.endUpdates
end
Your MyArray contains an array of objects. The method insertRowsAtIndexPaths:withRowAnimation is for telling the tableView that you have added items to your datasource and that it should go and ask for that data at the indexPaths you supply.
The steps you take are:
Update your data model
Tell the tableView what changes have been made
So for example if you start with an array and add one object your would need to do
# Start with empty data model
my_array = []
# Update the model to have one object
my_array << my_new_object
# Tell the table that you have updated your model by inserting one object at the beginning of the first section
inserted_index_paths = [ NSIndexPath.indexPathForRow(0, inSection: 0]) ]
self.tableView.insertRowsAtIndexPaths(inserted_index_paths, withRowAnimation: UITableViewRowAnimationFade)
If you then inserted an object at the end again it would look very similar except that the newly inserted index path would be NSIndexPath.indexPathForRow(1, inSection: 0) as you would now be telling the tableView that you inserted a second object into the first section
Old Answer
You should do something like
index_paths = MyArray.map { |index| NSIndexPath.indexPathForRow(index, inSection: 0) }
self.tableView.beginUpdates
self.tableView.insertRowsAtIndexPaths(index_paths, withRowAnimation: UITableViewRowAnimationFade)
self.tableView.endUpdates
in obj-c this is the way to do it...
if you get your datas from an NSMutableArray, just make your operation in it.
for example, i got
array = #[#"text1", #"text2", #"text4"];
for my cell, i'm calling in cellForRowAtIndexPath method:
[cell setText:array[indexPath.row]];
in my array i'm making :
array[2] = #"text3";
and then :
[self.tableView reloadData];
I didn't test this but it's the good way to insert a new row.