Clickable UITableView section - ios

I have a sectioned UITableView. Rather than have each row of the section individually selectable, how to I make it so the entire section (rows plus header view) respond the same if there is a tap anywhere in the section. Suggestions?

This kind of behavior can be supported in different ways. The simplest way to make an entire section have the same behavior is as follows:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section {
case 0:
doBehavior0()
break
case 1:
doBehavior1()
break
// etc...
}
}
Keep in mind that this will not work for headers. You'll have to create a gesture recognizer and call doBehaviorX() from that. More details here. This should be simple enough to implement if you have a fixed number of sections (which I will assume since your question didn't specify).

Related

How to left/right-align custom UITableView cell

I’m coding a “chatbot” app, and to hold the message bubbles I have a UITableView and a custom message-bubble shaped cell. Here’s what it looks like so far:
All the cells will look the same, except I’d like every other cell to be, say, half the width of the table and alternating right/left aligned. How could I do this programmatically?
The better way - to create two classes InMessageCell, OutMessageCell, and add all properties (like aligning of all elements) hide inside of this cell. Both cell will have the same width, but all bubbles will be moved on one or other side. It may inheritance from the main class MessageCell, so all logic may stay in main class, but UI part - splitted up.
Two straightforward ways of achieving this by using custom table view cells:
Have two different cell classes. They can share much of their implementation (e.g. by class heritage), but differ in their layout (one is left aligned, one right aligned). For each row in your table, decide which cell you need to use, and dequeue the appropriate one.
Have one cell class whose layout can be toggled when it's being dequeued. (How exactly this toggle looks like depends of course on how you chose to layout your cell. It could e.g. be that you exchange the constant to an autolayout constraint you reference via #IBOutlet, the switching of a background image, or changing the alignment property of a stack view, among many others.)
Both ways depend on making a decision which cell flavor to use in your UITableViewDataSource's tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) function.
Here is a slightly abstract example using the first method. In your UITableViewDataSource:
enum ChatCellAlignment {
case left
case right
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellAlignment: ChatCellAlignment = self.cellAlignmentForIndexPath(indexPath) // you need to implement something like this
var identifier = "Default"
switch cellAlignment {
case .left:
identifier = "LeftAlignedBubbleCell"
case .right:
identifier = "RightAlignedBubbleCell"
}
let cell = self.tableView.dequeueReusableCell(withIdentifier: identifier)
if let cell = cell as? ChatBubbleCell { // assuming your custom cell classes all inherit from a "ChatBubbleCell"
cell.set(text: self.textForIndexPath(indexPath))
... // whatever setup you need to do on your custom cell
}
return cell
}
You can give the table view cell a value to know it. Then you can use autolayout (SnapKit) to make it align left or right

Disable move last row using Swift and UITableview

I am an iOS developer and I am currently developing an app using UITableView. The last row of the UITableView contains a button.
But there was a problem here. I put the move (moveRowAt) function in the row, and I can move to the last row! The last row should not be moved. It should always be located in the last row. The last row must have 'moveRowAt' disabled.
I looked up a lot of information, including Stack Overflow, but I did not get any clues.
maybe I could do a bit more than a plain comment by making a sample implementation of making the last row immovable in every section you may have:
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 != indexPath.row
}
NOTE: if you have your datasource and the number of data accessible anywhere else, it would be more desirable to get the number of rows (in section) from there not by calling the delegate method (as I did here) – however, logically there is nothing wrong with any of the concepts; by the way: here is the class-ref docs of tableView(_:canMoveRowAt:), of you want to know more about it.
At this point, you need to tell the table view that the last row should not be moved. You could achieve this by implementing tableView(_:canMoveRowAt:) data source method:
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
// '??' means the last row...
return indexPath.row == ?? ? false : true
}
assuming that you are already know the number of rows, you should check the last one.
Furthermore: for your case it might be a good idea to the button as tableFooterView instead of row, therefore there is no need to implement the above method.

Does indexpath.row take care of iterating an array?

I'm working through a UITableView tutorial and I've become curious about array iteration as I implement the UITableViewDataSource methods. When we call indexPath.row, is this calling a for loop for us behind the scenes? I'm asking because months back when I was learning to use data from a webservice (I've forgotten the exact steps of how I did it precisely) but I believe I needed to iterate over the array in order to present the information in the console.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an instance of UITableViewCell, with default appearance
let cell = UITableViewCell.init(style: .value1, reuseIdentifier: "UITableViewCell")
// Set the text on the cell with the description of the item instance that is at the nth index of the allItems array, where n = row this cell will appear in on the tableview
let item = itemStore.allItems[indexPath.row]
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "$\(item.valueInDollars)"
return cell
}
Am I correct in thinking that the indexPath.row method is iterating over the array for us behind the scenes?
let item = itemStore.allItems[indexPath.row]
No, calling indexPath.row does not iterate through all rows for you. It's just a variable sent to cellForRowAt that you can access and use to create your cell. It contains information about which row in which section the function cellForRowAt was called for.
However, cellForRowAt is actually called every time a cell is going to be visible on your tableVIew. Imagine you have a dataSource with 100 items and it is possible to only see 10 cells at a time. When the tableView gets initially loaded, cellForRowAt will get called 10 times. Then, if you scroll your tableView to show 3 more cells, cellForRowAt will get called 3 more times.
First of all the tutorial seems to be pretty bad. Table view cells are supposed to be reused
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
The workflow to display table view cells is:
The framework calls numberOfRowsInSection (and also numberOfSections)
For each section/row pair in the range of the visible cells it calls cellForRowAt and passes the index path in the second parameter.
No. The indexPath is just a struct with a section and a row. You use those to directly look up the information from your array. No iteration is happening.
cellForRowAt is only called for the cells that are on screen, so the order they are called depends on which direction you are scrolling.
In fact, the UITableView doesn't even know if you are using an array, or generating the information on the fly. What you do with the indexPath row and section is up to you.
Check the documentation of UITableView and cellForRowAtIndexpath functions.
An index path locating a row in tableView.
IndexPath will be a structure, which helps you to get the specific location from a nested array. In your case of the table view, rows inside the section.
cellForRow will return a cell for particular index path(in the specified section and row)
So indexPath.section will give the section number for which the cell is going to be modified and returned to the data source. And Inside the section, and indexPath.row will be the corresponding row inside that section.
index path documentation
tableView-CellForRowAt: IndexPath
UITableView DataSource Documentation
[update]
If you want to add multiple sections, add another dataSource numberOfSections, and return the number of sections count from it.
I can link to a create a table view tutorial from Apple, but it's a long document, you can skip the starting and read the To display a section in your table view.
If you want, you can use a 2D array for keeping sections and rows. And in numberOfSections method return array.count, and in numberOfRows:inSection, return array[section].count
More examples,
example
example
thanks,
J

How to add a button to the end of UIScrollView to load more?

I have a scroll with paging. I want to load only 75 pages at a time and when the user reach the last page a button showes up with the last swipe. The button has to be clicked to load more.
The same like ios Zillow app as you see in the attached image.
How should I add this button? Is it part of the scroll or is it just a button out side the scroll?
So let's first think about how we initialize our table view. We use
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
to return the number of cells we want in the table, so let's say we use something like
return searchResultsArray.count
but if we want a "Load More Results" button, then let's add 1 more cell
return searchResultsArray.count + 1
Okay great, now how do we add the button in this new cell? We want to use
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
and we'll say
if indexPath.row == searchResultsArray.count
(remember that an array's count value is 1 greater than the index of the last element in the array, so this if statement is basically saying, "if this cell is the load more cell")
then we will create our button in this cell, and we can link a function to the button (using a selector) which will add results to the searchResultsArray, and then reload the table (using tableView.reloadData())
This is how you would approach this if it were a (vertical scrolling) table view, but if you want to recreate the horizontal scrolling interface in zillow, then you need to use a collection view, which is a bit complicated if you're new to iOS development, but still use the basic concept I described above. Here's a great video to help you with that: https://www.youtube.com/watch?v=Ko9oNhlTwH0
I will recommend you to go for UICollectionView rather than UIScrollView as with UIScrollView you will create multiple objects for your UIImageView and ad it in your UIScrollView which will take more memory as compare to UICollectionView, as with UICollectionView you can reuse the same cell so the memory consumed by UICollectionView will be very less as compare to UIScrollView.
Regrading adding button at the end, create two custom cell in your UICollectionView one is to show UIImageView and one is to show Load More button, fetch 75 results at a time and in the datasource numberOfItemsInSection return 75+1 (i.e. your array count + 1 [+1 to show load more button when you want to show it])
Now in cellForItemAtIndexPath compare if the indexpath.row count is greater than your array count then load the cell for Load More button.

Using indexPath vs. indexPath.row in tableView.deselectRowAtIndexPath

I noticed in that to disable the highlight feature of a table cell you would yse the following method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
...
tableView.deselectRowAtIndexPath(indexPath, animated: false)
}
What I don't understand is what's going on in the first argument indexPath. In the instructional book I've been reading just about every other method has been using indexPath.row for selecting individual cells:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as CustomTableViewCell
cell.nameLabel.text = restaurantNames[indexPath.row]
cell.typeLabel.text = restaurantTypes[indexPath.row]
cell.locationLabel.text = restaurantLocations[indexPath.row]
}
Just out of curiosity I tried passing indexPath.row as an argument but got the error, 'NSNumber' is not a subtype of 'NSIndexPath'. In this particular case what is the main difference between using indexPath and indexPath.row.
From the NSIndexPath class reference:
The NSIndexPath class represents the path to a specific node in a tree
of nested array collections. This path is known as an index path.
Table views (and collection views) use NSIndexPath to index their items
because they are nested structures (sections and rows). The first index is the section of the tableview and the second index
the row within a section.
indexPath.section and indexPath.row are just a "convenience" properties, you always have
indexPath.section == indexPath.indexAtPosition(0)
indexPath.row == indexPath.indexAtPosition(1)
They are documented in NSIndexPath UIKit Additions.
So NSIndexPath is an Objective-C class and is used by all table view
functions. The section and row property are NSInteger numbers.
(Therefore you cannot pass indexPath.row if an NSIndexPath is expected.)
For a table view without sections (UITableViewStyle.Plain) the
section is always zero, and indexPath.row is the row number of
an item (in the one and only section). In that case you can use
indexPath.row to index an array acting as table view data source:
cell.nameLabel.text = restaurantNames[indexPath.row]
For a table view with sections (UITableViewStyle.Grouped) you have
to use both indexPath.section and indexPath.row (as in this
example: What would be a good data structure for UITableView in grouped mode).
A better way to disable the selection of particular table view rows is
to override the willSelectRowAtIndexPath delegate method:
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
// return `indexPath` if selecting the row is permitted
// return `nil` otherwise
}
In addition, to disable the appearance of the cell highlight on touch-down, you can also set
cell.selectionStyle = .None
when creating the cell. All this is (for example) discussed in
UITableview: How to Disable Selection for Some Rows but Not Others.
Your question and confusion stems from the fact that Apple decided to use somewhat imprecise method name/signature
tableView.deselectRowAtIndexPath(indexPath, animated: false)
The "Row" word in the method signature implies that row is going to be deselected, therefore one would expect a row as a method parameter. But a table view can have more than one section. But, in case it doesn't have more sections, the whole table view is considered one section.
So to deselect a row (a cell really) in table view, we need to tell both which section and which row (even in case of having only one section). And this is bundled together in the indexPath object you pass as a parameter.
So the before-mentioned method might instead have a signature
tableView.deselectCellAtIndexPath(indexPath, animated: false)
which would be more consistent, or another option, albeit a bit less semantically solid (in terms of parameters matching the signature)
tableView.deselectRowInSectionAtIndexPath(indexPath, animated: false)
Now the other case - when you implement the delegate method
tableview:cellForRowAtIndexPath
here again (Apple, Apple...) the signature is not completely precise. They could have chose a more consistent method signature, for example
tableview:cellForIndexPath:
but Apple decided to stress the fact that we are dealing with rows. This is an 8 year old API that was never the most shining work of Apple in UIKit, so it's understandable. I don't have statistical data, but I believe having a single section (the whole tableview is one section) is the dominant approach in apps that use a table view, so you don't deal with the sections explicitly, and work only with the row value inside the delegate method. The row often serves as index to retrieve some model object or other data to be put into cell.
Nevertheless the section value is still there. You can check this anytime, the passed indexPath will show the section value for you when you inspect it.
NSIndexPath is an object comprised of two NSIntegers. It's original use was for paths to nodes in a tree, where the two integers represented indexes and length. But a different (and perhaps more frequent) use for this object is to refer to elements of a UITable. In that situation, there are two NSIntegers in each NSIndexPath: indexPath.row and indexPath.section. These conveniently identify the section of a UITableView and it's row. (Absent from the original definition of NSIndexPath, row and section are category extensions to it).
Check out tables with multiple sections and you'll see why an NSIndexPath (with two dimensions) is a more general way to reference a UITableView element than just a one-dimensional row number.
i see the perfect answer is from apple doc
https://developer.apple.com/documentation/uikit/uitableview/1614881-indexpath
if you print indexpath you may get [0,0] which is first item in the first section
I was confused between indexPath and indexPath.row until i understand that the concept is the same for the TableView and the CollectionView.
indexPath : [0, 2]
indexPath.section : 0
indexPath.row : 2
//
indexPath : the whole object (array)
indexPath.section : refers to the section (first item in the array)
indexPath.row : refers to the selected item in the section (second item in the array)
Sometimes, you may need to pass the whole object (indexPath).

Resources