Swift 2.0 UITableView Section Header scroll to top when tapped - ios

So I have checked everywhere and can't seem to find the answer I'm looking for. Here is my issue: I have a UITableView with different sections in it. Each section has a header that when tapped on, it expands that section and reveals it's rows or cells. However, when you tap on the header, it expands it's section down, but it stays in it's spot. I want that section header to move to the top when clicked. Below is the example code. I hope I explained this well.
Here is the section header itself:
func configureHeader(view: UIView, section: Int) {
view.backgroundColor = UIColor.whiteColor()
view.tag = section
let headerString = UILabel(frame: CGRect(x: 40, y: 15, width: tableView.frame.size.width-10, height: 40)) as UILabel
headerString.text = sectionTitleArray.objectAtIndex(section) as? String
headerString.textAlignment = .Left
headerString.font = UIFont.systemFontOfSize(24.0)
view .addSubview(headerString)
let frame = CGRectMake(5, view.frame.size.height/2, 20, 20)
let headerPicView = UIImageView(frame: frame)
headerPicView.image = headerPic
view.addSubview(headerPicView)
let headerTapped = UITapGestureRecognizer (target: self, action:"sectionHeaderTapped:")
view .addGestureRecognizer(headerTapped)
}
Here is the sectionHeaderTapped function:
func sectionHeaderTapped(recognizer: UITapGestureRecognizer) {
print("Tapping working")
print(recognizer.view?.tag)
let indexPath : NSIndexPath = NSIndexPath(forRow: 0, inSection:(recognizer.view?.tag as Int!)!)
if (indexPath.row == 0) {
var collapsed = arrayForBool .objectAtIndex(indexPath.section).boolValue
collapsed = !collapsed;
arrayForBool .replaceObjectAtIndex(indexPath.section, withObject: collapsed)
//reload specific section animated
let range = NSMakeRange(indexPath.section, 1)
let sectionToReload = NSIndexSet(indexesInRange: range)
self.tableView .reloadSections(sectionToReload, withRowAnimation:UITableViewRowAnimation.Fade)
}
}
Thanks!!

You can use scrollToRowAtIndexPath(_:atScrollPosition:animated:) on the table view to scroll the first row of said section to the top position.

One good way is to use IndexPath with a value of row "NSNotFoud", to go to the top of the section 😁
let indexPath = IndexPath(row: NSNotFound, section: 0)
self.scrollToRow(at: indexPath, at: .top, animated: true)

Related

CollectionView -Stop/Prevent Section Header from Scrolling Beyond a Certain Point

I have a collectionView that is pinned to the top of the view controller with a no navigationBar collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true.
It has a sticky header let headerLayout = cv.collectionViewLayout as? UICollectionViewFlowLayout; headerLayout?.sectionHeadersPinToVisibleBounds = true
The collectionView has 2 sections, the first section has no header but the second section does have a header. The issue is because the collectionView isn't pinned to the safeAreaLayoutGuide.topAnchor and there isn't a navigationBar, when I scroll, the header in the second section gets pinned to the very top of the screen behind the status bar.
How can I prevent the header from scrolling beyond a certain point. For example if I had a button pinned to the top of the screen, the header would stop once it hit the bottom of the button
myButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 50).isActive = true
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.contentInsetAdjustmentBehavior = .never
let secondIndexPath = IndexPath(item: 0, section: 1)
collectionView.layoutIfNeeded()
if let headerFrameInCollectionView = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: secondIndexPath), let window = UIApplication.shared.windows.first(where: \.isKeyWindow) {
let headerFrameInSuperView = collectionView.convert(headerFrameInCollectionView.frame, to: collectionView.superview)
let headerOriginY = headerFrameInSuperView.origin.y
let buttonFrame = view.convert(myButton.frame, to: window)
let bottomOfButton = buttonFrame.origin.y + buttonFrame.height
if headerOriginY == bottomOfButton {
collectionView.contentInset.top = headerOriginY // stop header from scrolling any further
} else {
collectionView.contentInset.top = 0
}
}
}
In short, to get this to work I had to set the collectionView.contentInset.top to whatever point I wanted section two's header to stop at but I also had to set the collectionView.contentOffset.y to a stop position.
if headerOriginY <= bottomOfButton {
collectionView.contentInset.top = bottomOfButton
collectionView.contentOffset.y = cellHeightFromSectionOne - bottomOfButton
} else {
collectionView.contentInset.top = 0
}
In long, I have 2 sections and what happens is if I set collectionView.contentInset.top = bottomOfButton in section 2 without changing the collectionView.contentOffset.y, the cell in section 1 scrolls off the screen and the whole process is very buggy. To get it to work:
1- get the height for the cell in the first section. Luckily for me this was easy because in section 1 the cell's height is the width of the screen and there is only 1 cell in section 1. In section 2 there aren't any cells so the cell size for that is .zero:
// size for the cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = UIScreen.main.bounds.width // the collectionView is the same width
if indexPath.section == 0 {
return CGSize(width: width, height: width)
}
return .zero // section 2 has no cells, just a header
}
2- In scrollViewDidScroll, this is where all the work is done. The variable names explains the process
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.contentInsetAdjustmentBehavior = .never
let secondIndexPath = IndexPath(item: 0, section: 1)
collectionView.layoutIfNeeded()
if let headerTwoAttributes = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: secondIndexPath), let window = UIApplication.shared.windows.first(where: \.isKeyWindow) {
let headerTwoFrameInSuperView = collectionView.convert(headerTwoAttributes.frame, to: collectionView.superview)
let headerTwoOriginY = headerTwoFrameInSuperView.origin.y
let buttonFrame = view.convert(myButton.frame, to: window)
let bottomPositionOfButtonInWindow = buttonFrame.origin.y + buttonFrame.height
let stopScrollingPositionY = bottomPositionOfButtonInWindow
let cellHeightFromSectionOne = UIScreen.main.bounds.width
let preventFirstCellInSectionOneFromScrollingPosition = cellHeightFromSectionOne - stopScrollingPositionY
if headerTwoOriginY <= stopScrollingPositionY {
collectionView.contentInset.top = stopScrollingPositionY
collectionView.contentOffset.y = preventFirstCellInSectionOneFromScrollingPosition
} else {
collectionView.contentInset.top = 0 // reset this to 0
}
}
}

How to prevent the last table cell from overlapping with the keyboard

I have a chat app that displays the messages in a table view. When I invoke the keyboard, I want:
The table view to scroll to the bottom
I want there to be no overlap between any messages and the keyboard.
#objc private func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let keyBoardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let keyboardViewEndFrame = view.convert(keyBoardFrame!, from: view.window)
let keyboardHeight = keyboardViewEndFrame.height
tableView.scrollToBottom() { indexPath in
let rectofCell = self.tableView.rectForRow(at: indexPath)
let lastCellFrame = self.tableView.convert(rectofCell, from: self.view.window)
if lastCellFrame.origin.y + lastCellFrame.size.height > keyboardViewEndFrame.origin.y {
let overlap = lastCellFrame.origin.y + lastCellFrame.size.height - keyboardViewEndFrame.origin.y
self.tableView.frame.origin.y = -overlap
}
}
}
}
extension UITableView {
func scrollToBottom(animated: Bool = true, completion: ((IndexPath) -> Void)? = nil) {
let sections = self.numberOfSections
let rows = self.numberOfRows(inSection: sections - 1)
if (rows > 0){
let indexPath = IndexPath(row: rows - 1, section: sections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
completion?(indexPath)
}
}
}
The value for rectOfCell shows (0.0, 5305.333518981934, 375.0, 67.33333587646484) and the converted value (0.0, 9920.000185648601, 375.0, 67.33333587646484) and the table view disappears out of the screen.
I only want to move the table view upward if the last message overlaps with the eventual position of the keyboard. For example, when the keyboard is invoked (either when the table view is already at the bottom or not at the bottom), the table view shouldn't move upward if the appearance of the keyboard doesn't cover the messages (i.e., there is only one message).
You don't need to manage tableView.frame for such a thing. You just need to make sure that when keyboard appears, tableView adds necessary contentInset value from bottom so that user can still see all the content inside tableView with the keyboard still on screen.
All you need is this -
// when keyboard appears
let insets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
// when keyboard disappears
let insets: UIEdgeInsets = .zero
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets

Corner radius not properly fit in dynamic grouped tableView swift

I have created a Grouped TableView dynamically based on data. Based on data tableView cell generated automatic height so every cell has different rowHeight. I have set it accordingly by using self.tableView.rowHeight = 50
But Issue is I am using corner radius, but I don't want to use corner radius on every cell.
I am using grayBox UIView and all cells displayed in it. Corner radius apply to start of cell or grayBox and only at end of cell of grayBox but it applied to every cell. How can I do that corner radıus apply on start and bottom?
viewDidLoad() code for tableView Row Height
self.tableView.rowHeight = 50
Dynamic Grouped TableView code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.backgroundColor = UIColor.clear
cell.selectionStyle = .none
let grayBox = UIView(frame: CGRect(x: 5, y: 0, width: self.view.frame.size.width - 11, height: 50))
grayBox.backgroundColor = ("#cfd8dc").toColor()
grayBox.layer.cornerRadius = 5
grayBox.layer.borderColor = UIColor(red:0.80, green:0.80, blue:0.80, alpha:1.0).cgColor
grayBox.layer.borderWidth = 1.0
cell.contentView.addSubview(grayBox)
return cell
}
1- Use dequeue
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
Instead of
let cell = UITableViewCell()
2- Clear subviews here by removing with tag
let grayBox = UIView(frame: CGRect(x: 5, y: 0, width: self.view.frame.size.width - 11, height: 50))
grayBox.tag = 333
cell.contentView.subviews.forEach {
if $0.tag == 333 {
$0.removeFromSuperview()
}
}
cell.contentView.addSubview(grayBox)
3- For corner raduis
if indexPath.row == 0 || indexPath.row == arr.count - 1 {
grayBox.layer.cornerRadius = 5
}
else {
grayBox.layer.cornerRadius = 0
}

Image and scrollView rendering making my tableview choppy

There may be no good solution for my problem, but I want to ask just in case.
I have a tableview in which each cell contains a horizontal scrollview of variable width. The width of each cell's scrollview depends on the number and sizes of the images for that cell. Everything works pretty well, but the tableview scrolls less smoothly than it could. I'm using Parse to retrieve the images (and pfquertableviewcontroller), but this question should apply to tableviews in general.
Here is my tableview code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
if var cell:MainFeedTableViewCell? = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) as? MainFeedTableViewCell{
if(cell == nil) {
cell = NSBundle.mainBundle().loadNibNamed("MainFeedTableViewCell", owner: self, options: nil)[0] as? MainFeedTableViewCell
}
cell?.parseObject = object
return cell
}
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if let c = (cell as? MainFeedTableViewCell){
c.setUpObject()//this is where images are set
}
}
override func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if let c = (cell as? MainFeedTableViewCell){
c.resetObject() //this sets most of the properties to nil
}
}
And here is the function in my cell where images are set
func setUpObject(){
//I left out several lines of code where label text is set from the parseObject that is set when the cell is created
//setting images **problems here**
if let numImages = self.parseObject?["numImages"] as? Int{
self.newWidth1 = self.parseObject?["width1"] as? CGFloat
self.newWidth2 = self.parseObject?["width2"] as? CGFloat
self.newWidth3 = self.parseObject?["width3"] as? CGFloat
self.newWidth4 = self.parseObject?["width4"] as? CGFloat
self.newWidth5 = self.parseObject?["width5"] as? CGFloat
if numImages == 1{
self.containerView = UIView(frame: CGRect(x: self.scrollView.frame.minX, y: self.scrollView.bounds.minY - 90, width: self.scrollView.frame.width, height: self.scrollView.frame.height))
self.image1 = PFImageView(frame: CGRect(x: self.scrollView.frame.minX , y: self.scrollView.frame.minY, width: self.scrollView.frame.width, height: self.scrollView.frame.height))
self.image1?.contentMode = UIViewContentMode.ScaleAspectFit
self.image1!.file = self.parseObject?["image"] as? PFFile
self.image1!.loadInBackground()
self.containerView!.addSubview(self.image1!)
self.scrollView.contentSize = self.containerView!.bounds.size
self.scrollView.addSubview(self.containerView!)
}
else if numImages == 2{
if self.newWidth1 + self.newWidth2 < self.scrollView.frame.width{
self.containerView = UIView(frame: CGRect(x: self.scrollView.frame.midX - (self.newWidth1 + self.newWidth2)/2, y: self.scrollView.bounds.minY - 90, width: (self.newWidth1 + self.newWidth2), height: self.scrollView.frame.height))
}
else{
self.containerView = UIView(frame: CGRect(x: self.scrollView.frame.minX, y: self.scrollView.bounds.minY - 90, width: (self.newWidth1 + self.newWidth2), height: self.scrollView.frame.height))
}
self.image1 = PFImageView(frame: CGRect(x: self.scrollView.frame.minX , y: self.scrollView.frame.minY, width: self.newWidth1, height: self.scrollView.frame.height))
self.image1!.file = self.parseObject?["image"] as? PFFile
self.image1!.loadInBackground()
self.containerView!.addSubview(self.image1!)
self.image2 = PFImageView(frame: CGRect(x: (self.scrollView.frame.minX + self.newWidth1), y: self.scrollView.frame.minY, width: self.newWidth2, height: self.scrollView.frame.height))
self.image2!.file = self.parseObject?["image2"] as? PFFile
self.image2!.loadInBackground()
self.containerView!.addSubview(self.image2!)
self.subLayer = CALayer()
self.subLayer.backgroundColor = UIColor.whiteColor().CGColor
self.subLayer.frame = CGRect(x: self.newWidth1, y: self.scrollView.frame.minY, width: 1, height: self.scrollView.frame.height)
self.containerView!.layer.addSublayer(self.subLayer)
self.scrollView.contentSize = self.containerView!.bounds.size
self.scrollView.addSubview(self.containerView!)
}
//repeat similar code for cases where there are 3, 4, or 5 images
There might be a fundamental issue with dynamically adjusting the size of the scrollview and adding it to superview just in time, but I'm trying to follow the design mockup that my designer gave me.
Here is what the scrollview on the cell looks like (with each image in the scrollview separated by a thin white line)
Remove your willDisplayCell and didEndDisplayingCell. That will fire as you scroll and your setUpObject code, while not huge, will block the main thread slightly. Instead move setUpObject() to right before returning the cell in cellForRowAtIndexPath.
Depending on how many rows you have and performance requirements you could also adjust the viewController to download all of the images ahead of time and pass them to the cell instead of loading them inside the cell.

How to populate UITableView from the bottom upwards?

is it possible to reverse the order of a tableView. I have searched a lot for a solution but all the results have not quite been a solution to what I am trying to achieve. They all suggest scrolling to the last position of a table with scrollToRowAtIndexPath and populating the data in reverse. But this doesn't work if the table content is dynamic and in some instances not all the cells have data. For example in a normal tableView the order is:
label 1
label 2
label 3
empty
empty
scroll direction
v
V
the desired result would be:
scroll direction
^
^
empty
empty
empty
label 3
label 2
label 1
in this example if I used the suggested method of scrollToRowAtIndexPath and use the length of the array of objects, I would only get the third cell from the top. And end up with something like this:
unwanted outcome:
label 3
label 2
label 1
empty
empty
scroll direction
v
V
any help would be great thank you.
To populate UITableView from the bottom:
- (void)updateTableContentInset {
NSInteger numRows = [self.tableView numberOfRowsInSection:0];
CGFloat contentInsetTop = self.tableView.bounds.size.height;
for (NSInteger i = 0; i < numRows; i++) {
contentInsetTop -= [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
if (contentInsetTop <= 0) {
contentInsetTop = 0;
break;
}
}
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0);
}
To reverse the order of elements:
dataSourceArray = dataSourceArray.reverseObjectEnumerator.allObjects;
Swift 4.2/5 version:
func updateTableContentInset() {
let numRows = self.tableView.numberOfRows(inSection: 0)
var contentInsetTop = self.tableView.bounds.size.height
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
break
}
}
self.tableView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
Swift 3/4.0 version:
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
first reverse uitableview
tableView.transform = CGAffineTransformMakeScale (1,-1);
then reverse cell in cell create.
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
...
cell.contentView.transform = CGAffineTransformMakeScale (1,-1);
Swift 4.0 and 4.2 version
First reverse UITableView in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
}
Then reverse the cell in cellForRowAt.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell else { fatalError() }
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
Here is a refined solution of KlimczakM´s solution that works with autolayouted tableview cells (as well as the fixed ones). This solution also works with sections, section headers and section footers.
Swift 3.0:
func updateTableContentInset(forTableView tv: UITableView) {
let numSections = tv.numberOfSections
var contentInsetTop = tv.bounds.size.height -
(self.navigationBar?.frame.size.height ?? 0)
for section in 0..<numSections {
let numRows = tv.numberOfRows(inSection: section)
let sectionHeaderHeight = tv.rectForHeader(inSection: section).size.height
let sectionFooterHeight = tv.rectForFooter(inSection: section).size.height
contentInsetTop -= sectionHeaderHeight + sectionFooterHeight
for i in 0..<numRows {
let rowHeight = tv.rectForRow(at: IndexPath(item: i, section: section)).size.height
contentInsetTop -= rowHeight
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
// Break outer loop as well if contentInsetTop == 0
if contentInsetTop == 0 {
break
}
}
tv.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
}
NOTE:
Above code is untested but should work.
Just make sure that you cope for the height of any navbar or tabbar and you'll be fine. In the code above i only do that for the navbar!
Don't bother to write the code by yourself.
Why don't you use ReverseExtension. Its very easy and will give you all required results. Please follow this url
https://github.com/marty-suzuki/ReverseExtension
Note: Whenever you need to add a new cell, please insert newly added model at zeroth index of datasource array, so new cell should add at bottom. Otherwise it would add the cell at top and you would get confused again.
The simple way is use like UILabel multiple lines + autolayout.
-> UITableView should resize it base on it content(aka intrinsic layout).
Create your tableview and set base class following:
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
}
}
Now set left right and bottom layout constraints for your tableview pin to it parent view.
The most important is top layout constraint should set to great than or equal 0, This condition guaranteed table will not tall than it parent view.
I did in cellForRowAt method:
let reverseIndex = myArray.count-indexPath.row-1
let currCellData = myArray.object(at: reverseIndex)
and then you continue working with currCellData
//Add these lines where you want to reload your tableView
let indexpath = IndexPath(row: self.Array.count-1, section: 0)
self.tableView.scrollToRow(at: indexpath, at: .top, animated: true)
self.updateTableContentInset()
//Add this function below
func updateTableContentInset() {
let numRows = self.tableView.numberOfRows(inSection: 0)
var contentInsetTop = self.tableView.bounds.size.height
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
break
}
}
self.tableView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
I you want that every new cell should appear from the bottom of the tableView, use this:-
Invert the tableView:
myTableView.transform = CGAffineTransform (scaleX: -1,y: -1)
Invert the cells also:
cell.contentView.transform = CGAffineTransform (scaleX: -1,y: -1)
Now populate your tabelViewDataSource in opposite direction, like if you are using an array, then you may do like this:
myTableViewData.insert(<Your New Array Element>), at: 0)
This solution adjust the content inset as the content size changes using KVO. It also takes the content inset into account when scrolling to top as simply scrolling to CGPointZero will scroll to the top of the content instead of scrolling to the top of the table.
-(void)startObservingContentSizeChanges
{
[self.tableView addObserver:self forKeyPath:kKeyPathContentSize options:0 context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:kKeyPathContentSize] && object == self.tableView)
{
// difference between content and table heights. +1 accounts for last row separator
CGFloat height = MAX(self.tableView.frame.size.height - self.tableView.contentSize.height, 0) + 1;
self.tableView.contentInset = UIEdgeInsetsMake(height, 0, 0, 0);
// "scroll" to top taking inset into account
[self.tableView setContentOffset:CGPointMake(0, -height) animated:NO];
}
}
Swift 3.01 - Other solution can be, rotate and flip the tableView.
self.tableView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(M_PI)))
self.tableView.transform = CGAffineTransform.init(translationX: -view.frame.width, y: view.frame.height)
UITableView anchor rows to bottom
If you have an array of object you display in cellForRowAtIndexPath:, lets say dataArray you can reverse it, or in cellForRowAtIndexPath:
you can do something like that:
NSString *yourObject = dataArray[[dataArray count] - 1 - indexPath.row];
cell.textLabel.text = yourObject
I assume you keep strings in dataArray.

Resources