Setting scrollView.delegate to scrollView and not UITableView - swift

I've set up a scrollView inside my custom cell. Inside my tableViewController I've used...
UIScrollViewDelegate
...Which fill delegate when the "scrollView" has moved. Though, I only want it to react when the image scroll view is moved.
It currently reacts to both, when the tablView scrolls and image scrollview scrolls.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//other irrelevant cell setup....
scrollView = cell.imageScrollView
scrollView.pagingEnabled = true
scrollView.scrollEnabled = true
scrollView.delegate = self
scrollView.tag = indexPath.row
}
func scrollViewDidScroll(scrollView: UIScrollView) {
var pageWidth = scrollView.frame.size.width
var newPage = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1
//prints: 0.0 , while tableView scrolls
//also, prints newPage when scrollView is moved
print(newPage)
}
I've tried to use sender but didn't know who to properly incorporate it. Any ideas? It seems that....
- (void)scrollViewDidScroll:(UIScrollView *)sender{}
... no longer exists.

You can try use isMemberOfClass(),like this:
if sender.isMemberOfClass(UITableView) {
// your logic
}
else sender.isMemberOfClass(UIScrollView)
{
// your logic
}

Related

How to update UITableview Header height while scrolling programatically

In My application I have top navigation bar and a tableview below the navigation bar. I have CollectionViewCell with two rows which added inside the UITableViewHeader programmatically. When ever I scroll the the TableView to top, i want the header to stop just below the navigation bar, and update the TableView Header height so I can show only one row. I just want to do an animation (like Shrinked)when the TableViewHeader sticks to the navigationbar the two collectionview rows should turn into one row by decreasing the Header Height. How can I do it programmatically
Below is my code for showing CustomHeaderView
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 183))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
Also i'm checking for the scroll position to set the tableview header height progmmatically which isn't successful for me.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 //This value is to check when the header reached the top position {
//Condition to check and animate the headerview height to make collectionview cell two rows into one rows.
}
How can I achieve the TableViewHeader height update when header sticks on top while scrolling.
Any help is appreciated.
What you are looking for is "sticky header"
and you want to change the header as well.
Sticky part is built in automatically I think if you just use UITableViewController(style: .plain), if that doesn't work for you, you can just google sticky header and there are lots of answers.
the part about changing the height or animating it. you are doing it right, just do something like:
// update your viewForHeader method to account for headerRows variable above
// update your viewForHeader method to account for headerRows variable above
// default 2, you modify this in your scroll
var headerRows = 2
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let height = headerRows == 2 ? 183 : 91
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: height))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 {
updatedHeader.frame.size.height = 40
self.tableviewObj.tableHeaderView = updatedHeader
headerRows = 1 } else {
headerRows = 2
}
self.tableView.reloadSectionHeaders()
}
If you want to do some animating instead, what you would do is store reference to your headerView in a variable of your view controller and inside your scrollViewDidScroll animate it using UIView.animate{...}
hope this helps man.

TableView reloadData resets scrollView contentOffset in header cell

I have a tableView with two custom cells. One for the header and one for the cells inside the tableView.
The Header cell looks like this:
Label Label ScrollView(inside scrollView is an imageView)
The other cell looks like this:
Label TextField ScrollView(inside scrollView is an imageView)
When I scroll one cell horizontally all other cells (including header cell) will get the same contentOffset. This works like a charm.
I added another function which adds another imageView inside the scrollView on top of the existing imageView for all cells except the header cel. Inside the new imageView I add a line which is draggable. This is realized by a longPressureGesture. Inside this gesture I need to do a tableView.reloadData() so that each time the line moves it will be updated for all cells. LongPressureGesture allows me to do this without loosing the control of the line while doing a reloadData().
But here is my problem:
The contentOffset of the scrollView inside the headerCell is reseted after calling reloadData() inside the longPressureGesture. But the contentOffset for all other scrollViews in the cells are still the same.
I tried to add the contentOffset in the headerCell so that each time the reloadData is called the contentOffset will be set. But this is not working because the contentOffset will be called to early and has no effect.
If I add a delay and then set the contentOffset again it is working. But this ends up in flickering which is not good.
Edit: Tried to use only one cell (used cell for header instead of specific header cell). Ended up with the same result. So this is an general issue for headers in tableView?
My code for the cells is:
/*
* This method fills all the given information for each signal to a custom cell by type SignalCell.
* Each cell has a signalName, a value at specific time and a wave image.
*/
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! SignalCell
cell.delegate = self
// Get the signal for this row
let signal = VCDHelper.sharedInstance.signals[indexPath.row] as! VCDSignal
// Set the name of the signal to the label
cell.signalLabel.text = signal.name
cell.name = signal.name
// cellContainer is set inside renderWave every time a new Cell appears
renderTimePicker(cell, signal: signal, indexPath: indexPath)
renderWave(cell, signal: signal, indexPath: indexPath)
cellContainer.setObject(cell, forKey: signal.name)
return cell
}
Code for header:
// Display header cell
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCellWithIdentifier(cellHeaderIdentifier) as! HeaderCell
// Add object to renderer dictionary
var headerRenderer = headerToRender.objectForKey("header") as? TimeRenderer
// Render waves
if let headerRenderer = headerRenderer {
cell.timeImageView.image = headerRenderer.renderedTime()
} else {
// There is no renderer yet
headerRenderer = TimeRenderer()
headerToRender.setObject(headerRenderer!, forKey: "header")
// Creates and returns an NSBlockOperation object with block
let operation = NSBlockOperation(block: { () -> Void in
let renderedHeaderImage = headerRenderer!.renderedTime()
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
cell.timeImageView.image = renderedHeaderImage
cell.cellContainer = self.cellContainer
})
})
queue.addOperation(operation)
headerToRenderOperations.setObject(operation, forKey: "header")
}
cellContainer.setObject(cell, forKey: "header")
return cell
}
And here the code for saving/setting the contentOffset for each Cell realized in SignalCell class:
/*
* This method allows only horizontal scrolling.
* It also handles that scrolling out of bounds is not allowed
*/
func scrollViewDidScroll(scrollView: UIScrollView) {
// Set contentOffsetX to minimum bound value because we are out of range
if scrollView.contentOffset.x < 0 {
scrollCells(scrollView, contentOffsetX: 0.0)
VCDHelper.sharedInstance.waveScrollContentOffsetX = 0.0
}
if scrollView.contentOffset.x > 0 {
// ContentOffsetX is the point at the beginning of the scrollView
// We know the total size of the image and need to subtract the size of the scrollView
// This will result in the max contentOffset.x for scrolling
if scrollView.contentOffset.x <= CGFloat(VCDHelper.sharedInstance.imageWidth) - scrollView.bounds.width {
scrollCells(scrollView, contentOffsetX: scrollView.contentOffset.x)
VCDHelper.sharedInstance.waveScrollContentOffsetX = Float(scrollView.contentOffset.x)
} else {
// Set contentOffsetX to maximal bound value because we are out of range
scrollCells(scrollView, contentOffsetX: CGFloat(VCDHelper.sharedInstance.imageWidth) - scrollView.bounds.width)
}
}
}
/*
* This method scrolls all visible images inside the scrollView at once
*/
func scrollCells(scrollView: UIScrollView, contentOffsetX: CGFloat) {
for (key, cell) in self.cellContainer {
if key as! String == "header" {
let headerCell = cell as! HeaderCell
let scrollContentOffsetY = headerCell.timeScrollView.contentOffset.y
// Dont use setContentOffset because this will call scrollViewDidScroll each time
headerCell.timeScrollView.contentOffset = CGPoint(x: contentOffsetX, y: scrollContentOffsetY)
} else {
let signalCell = cell as! SignalCell
let scrollContentOffsetY = signalCell.signalScrollView.contentOffset.y
// Dont use setContentOffset because this will call scrollViewDidScroll each time
signalCell.signalScrollView.contentOffset = CGPoint(x: contentOffsetX, y: scrollContentOffsetY)
}
}
}

IOS Issue Getting Index Path Row at Specific Point

I have a view controller that contains a navigation bar, a table view and a tool bar. I included the UITableViewDelegate in the view controller, and correctly assigned the table's data source and delegate to the view controller through the storyboard. The table view loads its data from a remote database, once the table scrolls to the last cell more data is loaded into the table. I achieved this by using the scrollViewDidScroll and indexPathForRowAtPoint methods as outlined in the following post: How to know when UITableView did scroll to bottom in iPhone. However, when I run the app and scroll through the table the only index path returned by indexPathForRowAtPoint is the one that was located at the specified point at the time of table load. Here is the code and the output I get when I scroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint bottomPoint = CGPointMake(160, 430);
CGPoint topPoint = CGPointMake(160, 10);
NSLog(#"%d", [[_tableView indexPathForRowAtPoint:bottomPoint] row]);
}
Every time I scroll the following is outputted:
2013-06-08 00:56:45.006 Coffee[24493:907] 3
2013-06-08 00:56:45.012 Coffee[24493:907] 3
2013-06-08 00:56:45.040 Coffee[24493:907] 3
2013-06-08 00:56:45.069 Coffee[24493:907] 3
2013-06-08 00:56:45.088 Coffee[24493:907] 3
2013-06-08 00:56:45.105 Coffee[24493:907] 3
2013-06-08 00:56:45.135 Coffee[24493:907] 3
2013-06-08 00:56:45.144 Coffee[24493:907] 3
2013-06-08 00:56:45.173 Coffee[24493:907] 3
2013-06-08 00:56:45.180 Coffee[24493:907] 3
Where 3 is the indexPath.row of the cell the bottom point is on when the controller loads. What am I doing wrong and why is this happening? Does it have anything to do with the fact that the UITableView is located inside a parent view controller?
bottomPoint is looking for a location inside your scrollView. Your scrollView contains a table, and all the cells are in the SAME PLACE all the time, relative to your scrollView. That is why you always get the same cell at that point.
When you scroll, the cells don't move in the scrollView, the scrollView "moves" relative to its parent. This is done by having its contentOffset changed.
If you add your scrollView's y content offset to your bottomPoint, you'll get the point that you are probably really looking for in your scrollView.
like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint bottomPoint = CGPointMake(160, 430) - scrollView.contentOffset.y;
CGPoint topPoint = CGPointMake(160, 10);
NSLog(#"%d", [[_tableView indexPathForRowAtPoint:bottomPoint] row]);
}
As you can see from the Apple's Documentation, the Method indexPathForRowAtPoint: is refer to a point in the local coordinate system of the receiver (the table view's bounds), BUT NO the table view's frame. As the bottomPoint you set is in the table view's frame coordinate system, you will not get the right indexPath. HalR's answer gives the bottomPoint in the table view's bounds coordinate system.
indexPathForRowAtPoint:
Returns an index path identifying the row and section at the given point.
- (NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point
Parameters
point:
a point in the local coordinate system of the receiver (the table view's bounds).
Return Value
An index path representing the row and section associated with point or nil if the point is out of the bounds of any row.
FYI different between Frame and Bounds
Here is the class to implement the feature.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let flagView = UIView(frame: CGRect(x: 0, y: 100.0, width: self.view.frame.width, height: 20.0))
flagView.backgroundColor = .blue
self.view.addSubview(flagView)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Cell number: \(indexPath.row)"
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > 0 {
let currentPoint = CGPoint(x: self.view.frame.width - 100, y: 100.0 + scrollView.contentOffset.y)
print(" current index: \(String(describing: tableView.indexPathForRow(at: currentPoint)?.row))")
}
}
}
let bottomPoint = CGPoint(x: tableView.frame.midX, y: tableView.frame.maxY)
let converted = tableView.convert(point, from: tableView.superview)
let indexPathForBottomCell = tableView.indexPathForItem(at: converted)
This should work too, and may be easier to understand. All the additional spaces (like bottom inset) would be handled differently for each case.

UITableView Pagination

My app has custom UITableView cells. I want to show only one cell at a time - next cell should show partially. In ScrollView you can set isPagingEnabled to YES.
But how can i do above in UITableView?
Thanks
Note that UITableView inherits from UIScrollView, so you can set pagingEnabledto YES on the table view itself.
Of course, this will only work if all cells and the table view itself are of the same height.
If you want to always have a cell start at the top of the table view after scrolling, you could use a UIScrollViewDelegate and implement something like this.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
UITableView *tv = (UITableView*)scrollView;
NSIndexPath *indexPathOfTopRowAfterScrolling = [tv indexPathForRowAtPoint:
*targetContentOffset
];
CGRect rectForTopRowAfterScrolling = [tv rectForRowAtIndexPath:
indexPathOfTopRowAfterScrolling
];
targetContentOffset->y=rectForTopRowAfterScrolling.origin.y;
}
This lets you adjust at which content offset a scroll action will end.
Swift 5 basic version, but it does not work that well. I needed to customize it for my own use to make it work.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let tv = scrollView as? UITableView {
let path = tv.indexPathForRow(at: targetContentOffset.pointee)
if path != nil {
self.scrollToRow(at: path!, at: .top, animated: true)
}
}
}
Customized version
// If velocity is less than 0, then scrolling up
// If velocity is greater than 0, then scrolling down
if let tv = scrollView as? UITableView {
let path = tv.indexPathForRow(at: targetContentOffset.pointee)
if path != nil {
// >= makes scrolling down easier but can have some weird behavior when scrolling up
if velocity.y >= 0.0 {
// Assumes 1 section
// Jump to bottom one because user is scrolling down, and targetContentOffset is the very top of the screen
let indexPath = IndexPath(row: path!.row + 1, section: path!.section)
if indexPath.row < self.numberOfRows(inSection: path!.section) {
self.scrollToRow(at: indexPath, at: .top, animated: true)
}
} else {
self.scrollToRow(at: path!, at: .top, animated: true)
}
}
}
I don't think I'd use a UITableView for this at all.
I think I'd use a UIScrollView with a tall stack of paged content. You could dynamically rebuild that content on scrolling activity, so you mimic the memory management of UITableView. UIScrollView will happily do vertical paging, depending on the shape of its contentView's frame.
In other words, I suspect it's easier to make a UIScrollView act like a table than to make a UITableView paginate like scroll view.

How to set the height of table header in UITableView?

I have gone through Apple docs about UITableView class and delegate reference but couldn't find the way to set the table header height explicitly.
I set Table cell height using following delegate:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
and set section header/footer height using following delegates.
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
Could anyone please help me to set the table header/footer height?
Thanks.
Just set the frame property of the tableHeaderView.
I found a nice hack. Add the below line after modifying the frame propery
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
The trick is (I think) that the UITableView is caching the height (the frame actually) when you assign the view to the tableHeaderView property. The above line just assigns the height again.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeHeaderToFit()
}
private func sizeHeaderToFit() {
let headerView = tableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
More details can be found here
In case you still need it, have you tried to set the property
self.tableView.tableHeaderView
If you calculate the heigh you need, and set a new view for tableHeaderView:
CGRect frame = self.tableView.tableHeaderView.frame;
frame.size.height = newHeight;
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:frame];
It should work.
It works with me only if I set the footer/header of the tableview to nil first:
self.footer = self.searchTableView.tableFooterView;
CGRect frame = self.footer.frame;
frame.size.height = 200;
self.footer.frame = frame;
self.searchTableView.tableFooterView = nil;
self.searchTableView.tableFooterView = self.footer;
Make sure that self.footer is a strong reference to prevent the footer view from being deallocated
Swift 4 - you can manage height with HEIGHT_VIEW,Just add this cods, Its working
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let HEIGHT_VIEW = 60
tableView.tableFooterView?.frame.size = CGSize(width: tblView.frame.width, height: CGFloat(HEIGHT_VIEW))
tableView.tableHeaderView?.frame.size = CGSize(width:tblView.frame.width, height: CGFloat(HEIGHT_VIEW))
}
Just create Footer Wrapper View using constructor UIView(frame:_)
then if you are using xib file for FooterView, create view from xib and add as subView to wrapper view. then assign wrapper to tableView.tableFooterView = fixWrapper .
let fixWrapper = UIView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, 54)) // dont remove
let footer = UIView.viewFromNib("YourViewXibFileName") as! YourViewClassName
fixWrapper.addSubview(footer)
tableView.tableFooterView = fixWrapper
tableFootterCostView = footer
It works perfectly for me! the point is to create footer view with constructor (frame:_). Even though you create UIView() and assign frame property it may not work.
If add a view as table header view in IB, set the frame of that view in IB in Tab 5(size inspector)
If you programatically set the tableHeaderView, then just set it inside viewDidLayoutSubviews.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupTableViewHeader()
}
private func setupTableViewHeader() {
// Something you do to set it up programatically...
tableView.tableHeaderView = MyHeaderView.instanceFromNib()
}
If you didn't set it programatically, you need to do similar to what #Kris answered based on this link
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeHeaderToFit()
}
private func sizeHeaderToFit() {
if let headerView = tableView.tableHeaderView {
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
#kris answer is helpful for me anyone want it in Objective-C.
Here is the code
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self sizeHeaderToFit];
}
-(void)sizeHeaderToFit{
UIView *headerView = self.tableView.tableHeaderView;
[headerView setNeedsLayout];
[headerView layoutIfNeeded];
CGFloat height = [headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect frame = headerView.frame;
frame.size.height = height;
headerView.frame = frame;
self.tableView.tableHeaderView = headerView;
}
If you are using XIB for tableView's main headerView you can set XIB as a freeform set the Height as you want and unclick Autoresizing's top,bottom blocks and upper,lower arrows.Only horizontal pieces will be selected.Vertical will be unselected as I mentioned above.
You can create a UIView with the desired height (the width should be that of the UITableView), and inside it you can place a UIImageView with the picture of the proper dimensions: they won't stretch automatically.
You can also give margin above and below the inner UIImageView, by giving a higher height to the container view.
Additionally, you can assign a Translation transform in order to place the image in the middle of its container header view, for example.
With autolayout you could do something like:
tableView.sectionHeaderHeight = UITableViewAutomaticDimension
tableView.estimatedSectionHeaderHeight = <your-header-height>
or if your headers are of different heights, go ahead and implement:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return <your-header-height>
}
In Xcode 10 you can set header and footer of section hight from "Size Inspector" tab
If you changed height of tableView's headerView, just reset headerView's frame, then, reset headerView of tableView:
self.headerView.frame = newFrame;
self.tableView.tableHeaderView = self.headerView;
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
}
or you can use like this also
tableView.estimatedSectionHeaderHeight
Use table view default property :
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 35.0;
}
Thanks