Swift table view with few rows can be scrolled up - ios

I have this table view which in some cases contains few rows, even one.
What’s wrong with it is that even if I have one row, it allows me to scroll down and the row gets hidden at top.
It practically almost disappears from screen, as if there would be somethingto show below it.
I can’t disable scrolling because I have pull down to refresh.
Any ideas if there is a setting I am missing? Or how I could not allow scroll down if I do not have enough rows to cover the whole screen?

Actually, your case is kind of tricky, because:
The first I thought that the solution will be myTableView.alwaysBounceVertical = false
That's will do the job for you, but the problem in your case that you have a UIRefreshControl() and setting alwaysBounceVertical to false will disable scrolling to top for displaying the refreshController.
So, it should be done manually, as follows:
1- Implement the scrollViewDidScroll method from UIScrollViewDelegate.
2- check the scrolling direction in it.
3- if the scrolling direction goes down, check if content size of the tableView is more than its height, i.e check if tableView contains cell more than its height.
4- if the output of step 3 is false, disable scrolling, else, enable scrolling.
5- add dispatch_after to re-enable tableView scrolling.
It goes like this (Note: Swift 2 code.):
private var lastContentOffset: CGFloat = 0
// 1
func scrollViewDidScroll(scrollView: UIScrollView) {
// 2
if (self.lastContentOffset > scrollView.contentOffset.y) {
print("scrolling up")
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
print("scrolling down")
// 3 and 4
myTableView.scrollEnabled = myTableView.contentSize.height > myTableView.frame.size.height ? true : false
// 5
// delaying is half a second
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_MSEC) * 500), dispatch_get_main_queue(), { () -> Void in
self.myTableView.scrollEnabled = true
})
}
}
Hope this is a good solution for your case.

Related

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
}

How to scroll UICollectionView that is underneath another UICollectionView?

So heres my issue, the 4 orange rectangles you see on the gif are a single vertical UICollectionView = orangeCollectionView.
The Green and Purple "card" views are part of another UICollectionView = overlayCollectionView.
overlayCollectionView has 3 cells, one of which is just a blank UICollectionViewCell, the other 2 are the cards.
When the overlayCollectionView is showing the blank UICollectionViewCell, I want to be able to scroll the orangeCollectionView.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let superr = superview else { return true}
for view in superr.subviews {
if view.isKind(of: OrangeCollectionView.self) {
view.point(inside: point, with: event)
return false
}
}
return true
}
This allows me to scroll the orangeCollectionView HOWEVER this doesn't actually work to fix my issue. I need to be able to scroll left and right to show the cards, however this blocks all touches becuase the point always falls on the OrangeCollectionView.
How can I check to see if they are scrolling left/right to show the cards? Otherwise if they are on the blank cell, scroll the orangeViewController up and down.
Had this issue as well... Didn't find a nice way to do it, but this works.
First, you need to access the scroll view delegate method scrollViewDidScroll(). There you call:
if scrollView == overlayScrollView {
if scrollView.contentOffset.x == self.view.frame.width { // don't know which coordinate you need
self.overlayScrollView.alpa = 0
}
}
After that, you add a blank view onto of the orange collection view. The view's alpha is 0 (I think, maybe the color is just clear; try it out if it works).
In the code, you then add a UISwipeGestureRecognizer to the view you just created and and detect whether there's a swipe to the left or to the right.
By detecting the direction of that swipe, you can simply change the contentOffset.x axis of your overlayScrollView to 0 or self.view.frame.width * 2.
Sorry I can't provide my working sample code, I'm answering from my mobile. It's not the proper solution, but when I made a food app for a big client it worked perfectly and no one ever complained :)

Make a UITableView "sticky to the bottom"

Say you have a table view T which shows messages,
T[ message d
T[ message c
T[ message b
T[ most recent message down bottom
Say there are 100 messages, with the bottom 4 visible in the example.
So the table view height is 700 say. You have a typical text entry underneath...
T[ message d
T[ message c
T[ message b
T[ most recent message down bottom
[enter snapped chat message!] [send]
When the keyboard appears, the new height of the visible table view is 400 say.
Of course, that will "cut off the bottom" of the messages - the most recent two will no longer be visible.
T[ message d
T[ message c
[enter snapped chat message!] [send]
[ iOS keyboard ]
[ iOS keyboard ]
(So, messages A and B are now "under" the keyboard.)
Naturally what you do is just scroll the table after the keyboard appears, for example. No problem doing it in an ad-hoc manner.
However, it would be really natural if one could subclass UITableView in such a way that, as the size of the visible area changed, the table view knew to keep the "bottom point" identical.
So, as the bottom of the table moves up and down (due to keyboard appearing - or whatever cause), the table would scroll actually based on the movement of the "base" of the table.
(Apart from anything else this would solve the "match the animation timing" nuisance.)
Could this be achieved elegantly and if so how - indeed this would seem so natural these days, perhaps it's built-in to UITableView as a flag and I just don't know?
once again the question here is
How to modify UITableView so that it moves its own scroll position, as, the view size changes...
(so as to keep the "bottom point the same")
Note that it's trivial to just scroll the table "manually" as it were from the outside.
Section headers stick to the top, so maybe something like this:
1. Make the most recent message a section header view instead of a table cell
2. Mirror the table view vertically:
tableView.transform = CGAffineTransformMakeScale(1, -1);
3. Mirror vertically the section header and table cells
4. Reverse the order of your messages
Is that what you were looking for?
This is a way late answer, but I just solved the problem a bit more elegantly than the very ingenious accepted solution.
You'll need to have the following AutoLayout constraints (I'll omit the horizontal constraints):
UITableView.top = Safe Area.top
UITableView.bottom = MessageContainerView.top
MessageContainerView.bottom = Safe Area.bottom
MessageContainerView is where I keep the message text view and button. Now, in your ViewController add an IBOutlet for the bottom constraint of MessageContainerView:
#IBOutlet weak var messageContainerBottomConstraint: NSLayoutConstraint!
Next step is animating the bottom constraint's constant to be equal to the keyboard height + the safe area at the bottom and scrolling the table to the bottom when the keyboard is shown (add an observer for the notification as well):
#objc func showKeyboard(_ notification: NSNotification) {
guard let userInfo = notification.userInfo,
let targetFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let animationTime = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
else {
return
}
let safeAreaBottom = view.safeAreaInsets.bottom
messageContainerBottomConstraint.constant = -targetFrame.cgRectValue.height + safeAreaBottom
UIView.animate(withDuration: animationTime) {
self.view.layoutIfNeeded()
self.tableView.scrollToBottom()
}
}
Note that we use UIResponder.keyboardAnimationDurationUserInfoKey for the animation timing, so everything will be smooth.
Lastly, tableView.scrollToBottom() is an Extension for UITableView:
import UIKit
extension UITableView {
func scrollToBottom() {
let numberOfSections = self.numberOfSections
let numberOfRowsInSection = self.numberOfRows(inSection: numberOfSections - 1)
let indexPathForLastRow = IndexPath(row: numberOfRowsInSection - 1, section: numberOfSections - 1)
self.scrollToRow(at: indexPathForLastRow, at: .bottom, animated: false)
}
}
Let me know if you can try it, and if I missed anything.
(I did notice you asked for a way to modify the native UITableView behavior instead of "manually" scrolling, but I think this is a valid way of solving the problem)
As far as I know, you have a few choices for doing this:
If you have the indexPath of the row you want to be visible:
tableView.ScrollToRow(indexPath, UITableViewScrollPosition.Top, true);
This will scroll to the cell given by the indexPath. In this case you are selecting the option to show it on the .Top of the screen.
If you don't have the indexPath, you can use:
tableView.ScrollRectToVisible(new RectangleF(0, Ydisplacement, tableView.Width, tableView.Height), true);

Paging with UITableView Sections

My goal is basically to create a tableView with paging enabled that uses sections to determine the amount of pages, and which page you're on.
Example: 3 sections
1 section: 3 cells
2nd section: 14 cells - Scrolls to the very bottom of the section as a normal tableView would, when it hits the bottom, if you user continues swiping, goes to next page.
3rd section: 8 cells - Scrolls to bottom and stops.
So the first page would have 3 cells, then blank space, if you started to scroll up then the next section would appear, then that section header would snap to the top and you will have changed pages
The second page has many cells more than can fit on a single screen at one time, the user scrolls to the bottom of these, once the bottom is reached, can continue to scroll to reveal the third section header, then that header will snap to the top, etc
I know how to create a pages tableView, it is the paging out the tableview using sections rather than view height that I do not know is possible
I had to do something similar to your design.
I needed the first section to take up the entre screen and the second one to be an infinite scroll, with paging between Section 1 and Section 2 when scrolling down. Also, when scrolling up, I wanted the scroll to stop before the top section.
I know it is slightly different to your case, but you could quite easily figure out the height of each section and apply it similarly to my design.
here is how I did it :
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
if (lastContentOffset > offsetY) {
tableNode.view.pagingEnabled = offsetY < view.frame.height
}
lastContentOffset = offsetY
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
tableNode.view.pagingEnabled = offsetY < view.frame.height
}
Note that the scrollViewDidEndDecelerating is important to re-enable paging when scrolling DOWN.
Also notice I am only setting the tableNode.view.pagingEnabled when the user scrolls UP in the scrollViewDidScroll function.
Hope this helps anyone looking for this in the future, as I hope for you #YichenBman you have come up with a solution by now.

Custom table view scrolling animation

In my app I'm trying to implement custom table view scrolling. I'm trying to achieve something similar to Volvo ocean race app, which does it very well.
Basically when user scrolls, middle cell gets higher and current high cell gets smaller
My original idea was playing with cell's frame in scrollViewDidScroll method for example
func scrollViewDidScroll(scrollView: UIScrollView) {
if(self.tblViewPhotos.contentOffset.y > 0)
{
var thisCell = self.tblViewPhotos.cellForRowAtIndexPath(selectedIndexPath) as PhotoCell
var offset = self.tblViewPhotos.contentOffset.y;
thisCell.frame = CGRectMake(thisCell.frame.origin.x, thisCell.frame.origin.y, thisCell.frame.size.width, 250-offset);
}
}
however I'm getting very buggy results (frame changing suddenly when swiping fast etc). I would be glad, if someone can point me to right direction

Resources