Reordering sections in UITableView - ios

I have UITableView with sections and rows(https://imgur.com/a/hrYTEVR). I know how enable reordering for rows, but i dont know how implement reordering for sections.
I need add reordering control to sections(https://imgur.com/a/V5kU9Ew) and then when user touch this control, rows under section should flops. After that user can move section to new place.
After read topics on stackoverflow and other sites, i dont find any good idea how implement something like this.
I thought about implement sections through cells, but in this case i can't flop rows under section for further moving to new place.
If you have any idea how implement this – give me advice. Thanks!

There is no native functionality to achieve what you want. If I understand correctly you would want to collapse a whole section of rows and then start dragging the "header" around. If you want to do this on your own I would suggest starting with a pan gesture recognizer which triggers on the header button.
The gesture should be relatively obvious. After it starts on the header you need to track position using locationIn in your table view.
To collapse rows all you need to do is modify your table view cells with appropriate animation like:
tableView.beginUpdates()
tableView.deleteSections([myIndexPath], with: .top) // Maybe experiment with animation type
// Modify whatever you need to correspond this change in the data source
tableView.endUpdates()
Since you will be removing the section you will also be removing the view (header) which has the gesture recognizer. That means it might be better adding the gesture to the table view directly or its superview even. You will need to force it to trigger only when one of those buttons on headers is pressed. You can get some idea here about it. The rest is unaffected by this change.
At this point you will probably need to create an extra view which represents your section stack and follows your finger. This should be pretty easy if you add it as a subview and manipulate it's center with pan gesture recognizer locationIn in it's superview:
movableSectionView.center = panGestureRecognizer.location(in: movableSectionView.superview!)
So up to this point you should be able to grab a section which collapses all cells and be able to drag the "section stack" view around. Now you need to check where in table view your finger is to know where to drop the section. This is a bit painful but can be done with visibleCells and tableView.indexPath(for: ):
func indexPathForGestureRecognizer(_ recognizer: UIGestureRecognizer) -> IndexPath {
let coordinateView: UIView = tableView.superview! // This can actually be pretty much anything as long as it is in hierarchy
let y = recognizer.location(in: coordinateView).y
if let hitCell = tableView.visibleCells.first(where: { cell in
let frameInCoordinateView = cell.convert(cell.bounds, to: coordinateView)
return frameInCoordinateView.minY >= y && frameInCoordinateView.maxY <= y
}) {
// We have the cell at which the finger is. Retrieve the index path
return tableView.indexPath(for: hitCell) ?? IndexPath(row: 0, section: 0) // This should always succeed but just in case
} else {
// We may be out of bounds. That may be either too high which means above the table view otherwise too low
if recognizer.location(in: tableView).y < 0.0 {
return IndexPath(row: 0, section: 0)
} else {
guard tableView.numberOfSections > 0 else {
return IndexPath(row: 0, section: 0) // Nothing in the table view at all
}
let section = tableView.numberOfSections-1
return IndexPath(row: tableView.numberOfRows(inSection: section), section: section)
}
}
}
Once the gesture recognizer ends you can use this method to get the section you are dropping your items into. So just:
tableView.beginUpdates()
// Modify whatever you need to correspond this change in the data source
tableView.insertSections([indexPathForGestureRecognizer(panGestureRecognizer).section], with: .bottom)
tableView.endUpdates()
This should basically be enough for reordering but you might want to show in table view where the dragged section is. Like having a placeholder at the end of the section in which the stack will be dropped into. That should be relatively easy by simply adding and then moving an extra placeholder cell reusing indexPathForGestureRecognizer to get a position for it.
Have fun.

Related

Move UITableViewCell to top of UITableView

I am trying to move a UITableViewCell to the absolute top of the table (IndexPath 0,0). I have tried this:
let conversation = conversations[indexPath.row]
conversations.remove(at: indexPath.row)
conversations.insert(conversation, at: 0)
conversationsView.moveRow(at: indexPath, to: IndexPath(row: 0, section: 0))
This does what I want it to do: it moves the cell (currently in didSelectItemAt method for test purposes) to the top. However, it doesn't happen visually as I want it to happen.
What happens now is, the cell moves out to the bottom, and gets inserted from the top. What I'd like is some sort of animation that "moves" the cell from the current position (e.g. IndexPath 4,0 to the new position (IndexPath 0,0).
A perfect and accurate example of this is Facebook Messenger. When you are in the overview of all your conversations, and you receive a message in the third conversation, the cell moves "magically" and "visually" as you'd expect. This behavior I want to replicate.
Might be important: I use TextureGroup (AsyncDisplayKit) in my application. But this should behave like UITableView under the hood I guess.

Trouble Expanding UITableView

Goal & Background
I am trying to create an expandable UITableView using this tutorial. However, I want the table to update its container's height so that the new height matches the content. The problem is this creates a visual glitch on the last header (section) in the table— but only on the first time the animation is performed.
This is what it looks like: link
My thought is that as the table expands the hidden cells, the last row is pushed out of view. So when I update the height of the view, it has to redraw the last cell (notice the color change as it's reloaded). I'm not sure where the strange slide-in animation comes from though.
Question
How would I remove this glitch or better accomplish this task?
Code
Here is my hierarchy:
+-- ParentVC
| +-- ParentView
| | +-- CustomTableVC's View
| | | +-- Custom UITable
(CustomTableVC is a child of ParentVC)
This is how I reload the tapped section and set the new height
// === CustomTableVC === //
func toggleSection(_ header: PTTableHeader, section: Int) {
...
// Reload the section with a drop-down animation
table.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)
// Update the height of the container view
preferredContentSize.height = table.contentSize.height
}
// Height for section headers and rows: 44 (including estimated)
And here is how the parent is updated:
// === ParentVC === //
override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
super.preferredContentSizeDidChange(forChildContentContainer: container)
if let child = container as? PTTable {
customTableVC.view.layoutIfNeeded()
customTableViewHeightAnchor.constant = child.preferredContentSize.height
UIView.animate(withDuration: 3, animations: {
view.layoutIfNeeded()
})
}
// Height anchor starts at parentView's height / 3 because
// I'm not sure how to make it match the table's contentSize from the get-go
}
Removing view.layoutIfNeeded() causes the last section to not perform the slide-in animation but still glitch out.
Running on iPhone 11 Pro (simulator).
Reflection
I got it to work. The trick was actually pretty simple once I figured it out. I still think it's a little convoluted/smelly, but it works for my purposes and with the method I used to begin with.
Solution
Basically, I set the preferredContentSize before I reload the sections. This alerts the ParentVC to start animating before anything actually changes. This means that the table now has space to move the bottom section into without having to reload it.
Code
// === CustomTableVC === //
func toggleSection(_ header: PTTableHeader, section: Int) {
...
// Predict the height of the table BEFORE reloading it
predictHeight(section: section, willExpand: isExpanding) // Change isExpanding with whatever Bool is tracking the expand/collapse state of the section
// THEN reload the section with a drop-down animation
table.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)
// Optionally do the above with an animation using UIView.animate() or UIView.transition()
// And FINALLY update the height of the container view
// This one is probably optional, but it will be more exact depending on what methods you use to predict the height in predictHeight()
preferredContentSize.height = table.contentSize.height
}
func predictHeight(section: Int, willExpand: Bool) {
// Get the heights of all the known headers/footers/rows
let tableSectionsHeight = CGFloat(table.numberOfSections) * (table.estimatedSectionHeaderHeight + table.sectionFooterHeight)
let tableCellsHeight = CGFloat(table.visibleCells.count) * table.estimatedRowHeight
// Calculate the height of the section being expanded/collapsed
// With the method I used, I can't just do table.numberOfRows(inSection: Int) since expanding/collapsing is essentially just adding/removing those rows
// Instead I need to store a reference to the number of rows per section and access it via that array, object, etc.
let sectionContentHeight = willExpand ? CGFloat(rowCounts[section]) * table.estimatedRowHeight : 0 // 0 if collapsing
// Set the preferredContentSize so that the ParentVC picks it up
preferredContentSize.height = tableSectionsHeight + tableCellsHeight + sectionContentHeight
}

3D Touch Peek in transform-inverted table view cell

I have a chat application where I am using the inverted table view technique (table view is scaled to (1, -1) with a transform, and all the cells themselves are scale to (1, -1) as well, neutralizing the effect, thus effectively inverting the table view to display the "first" cell at the bottom) to "start" the table view from bottom. Everything works fine, except that I need to implement peek and pop gesture. I've connected a button inside my table view cell to the target view controller, and enabled peek and pop as shown:
The gesture works, though when I 3D touch partially and hold enough to stand out the touched item (but not pop the preview view controller) I am getting inverted view:
How can I make the view pop out with correct transform, while still using the inverted table view?
The reason why it shows the view upside down after a light press is not because it removes the transform from the cell. In fact the transform on the cell stays as it is but the segue pulls the cell view out of the context of the tableView. Therefore the transform on the cell is not counteracted by the transform on the table anymore.
I believe you could fix this by subclassing UIStoryboardSegue but it would probably be much easier to switch your method of starting the table at the bottom. Check out the accepted answer on this question.
I used a slightly modified version which accounts for the navigation bar and status bar:
func updateTableContentInset() {
let numRows = tableView(self.tableView, numberOfRowsInSection: 0)
var contentInsetTop = self.tableView.bounds.size.height - (self.navigationController?.navigationBar.frame.size.height)! - 20
for i in 0..<numRows {
let rowRect = self.tableView.rectForRow(at: IndexPath(item: i, section: 0))
contentInsetTop -= rowRect.size.height
if contentInsetTop <= 0 {
contentInsetTop = 0
}
}
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
}
Just add this to your UITableViewController and call it on viewDidLoad() and every time a new row is added to the table.
After adding a new row and then calling updateTableContentInset() you will have to scroll to the bottom like this:
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
Since there is nothing upside down here you will not have problems with your Peek segue.

UITableView load more data when scrolling to top?

How could I load more data while scrolling to the top without losing the current offset oh the UITableView?
Here is what I am trying to achieve:
This is the whole set of data:
row 1
row 2
row 3
row 4
row 5
row 6
row 7
row 8*
row 9
row 10
row 11
row 12
row 13
row 14
row 15
Now, imagine that the user loaded the ones marked in bold and the offset is at row 8, if the user scroll up and reached row 7, I want to load and insert the rows from 1 to 5 without jumping from row 7. Keeping in mind that the user may be scrolling so when data reached the phone it is at row 6, so I can't jump it back to row 7, but keep the scroll smooth and natural (just how happen when you load more data while scrolling down, that the data is reloaded without the tableview jumping from between rows).
By the way, by offset I mean the contentOffset property of the UITableView.
Thanks for your help, I do really appreciate it!
When you are updating your data, you need to get the current offset of the tableView first. Then, you can add the height of the added data to the offset and set the tableView's offset like so:
func updateWithContentOffsset(data: [String]) {
guard let tableView = tableView else {
return
}
let currentOffset = tableView.contentOffset
let yOffset = CGFloat(data.count) * tableView.rowHeight // MAKE SURE YOU SET THE ROW HEIGHT OTHERWISE IT WILL BE ZERO!!!
let newOffset = CGPoint(x: currentOffset.x, y: currentOffset.y + yOffset)
tableView.reloadData()
tableView.setContentOffset(newOffset, animated: false)
}
You can also take a look at the gist that I created. Simply open up a new iOS Playground and copy-paste the gist.
The only thing you have to be aware of is that make sure you know your row height to add to the offset.
#Pratik Patel Bellow answer is best one.
Right down or call bellow function in the web-service response.
func updateWithScroll(data: [String]) {
guard let tableView = tableView else {
return
}
guard let currentVisibleIndexPaths = tableView.indexPathsForVisibleRows else {
return
}
var updatedVisibleIndexPaths = [IndexPath]()
for indexPath in currentVisibleIndexPaths {
let newIndexPath = IndexPath(row: indexPath.row + data.count, section: indexPath.section)
updatedVisibleIndexPaths.append(newIndexPath)
}
tableView.reloadData()
tableView.scrollToRow(at: updatedVisibleIndexPaths[0], at: .top, animated: false)
}
Now, imagine that the user loaded the ones marked in bold and the offset is at row 8, if the user scroll up and reached row 7, I want to load and insert the rows from 1 to 5 without jumping from row 7.
Loading data into the table as its needed is one of the things that UITableView does for you automatically. Don't try to insert rows into the table because they'll soon be needed as the user scrolls toward them -- the table view will request those rows from its data source as they're needed. You just need to make sure that your data source has the information it needs in order to fulfill the tables requests for cells as they arrive.
Things get a little more complicated if populating the rows requires making a network request for each row. Given your use of "load" in the question, I don't think that's what you're talking about, but in case that's your situation, here are some tips:
Request the data for as many rows as you reasonable can as early as you reasonably can. The amount of data displayed in a single row is typically small, so requesting a few hundred rows all at once shouldn't be a big deal.
If you don't have the data you need for a given row, make the necessary request, but return a cell immediately. The cell you return could use a spinner or other indication that the data is pending. When the request completes, you can tell the table to reload the appropriate row so that the proper content will display.

UICollectionView - scroll to the next page

Is there any chance to scroll in the UICollectionView to the wanted item using . scrollToItemAtIndexPath and snap not to the item itself, but to the page the item is part of? (I got paging enabled.)
Cheers
You need to create NSIndexPath than scroll to that index.
//Mark: - Move to cell when view did appear
overload viewDidAppear() {
let scrollIndex: NSIndexPath = NSIndexPath(forItem: dayIndex, inSection: monthSection)
if (// Check you are in range of UIcollectionView's count) {
//Scroll collectionview according to your requirement i.e UICollectionViewScrollPositionCenteredHorizontally or UICollectionViewScrollPosition.Left
self.YourCollectionView.scrollToItemAtIndexPath(scrollIndex, atScrollPosition: UICollectionViewScrollPositionCenteredHorizontally, animated: true)
}
}
I did end up using offsetContent property;
I knew the width of every so called page.
I wrote a code to get the indexPath for current day
I retrieved the section of the indexPath
I multiplied the section by the width of the view and named it "offset"
I set my UICollectionView.contentOffset.x to "offset" and it works fine.
I also tried using .scrollRectToVisible and the offset, and it worked amazingly, but I also wanted to updated something that was based on the contentOffset of the UICollectionView, and the .scrollRectToVisible seems not to update this property - it was updated only after I dragged the view a little.
Thanks for all your help!

Resources