How to know that tableView started scrolling - ios

I have a doubt: is there any way to intercept a tableView scrolling to add it an action? For example my prototype cell background is red, touching up inside a cell its background color begin blue and scrolling the tableView background color return red.
Is it possible to do this?!
Thanks in advance.

UITableView inherits from UIScrollView and UITableViewDelegate extends UIScrollViewDelegate.
Particularly you may be interested in scrollViewDidScroll method. So, in your UITableViewDelegate implementation, add the following method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
NSLog("Table view scroll detected at offset: %f", scrollView.contentOffset.y)
}

In the ViewController that is set as the delegate for the tableView, you can also set the delegate methods of a scrollView. These will be called when the tableView is scrolled as it contains a scrollView.
e.g:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
}
}

You are better of keeping a variable around e.g. var isBackgroundColorRed: Bool = true
And another for scroll y position when you set the background color to blue. e.g.
var blueBackgroundColorYOffset: CGFloat?
When you set the background color to blue, set the y offset, to the contentView.origin.y.
Then in the delegate for the tableview (which subclasses UIScrollView)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !isBackgroundColorRed {
// apply some color blending between blue and red based on
// the amount of movement on y axis, until you reach the limit
// its also important to take the absolute value of the blueBackgroundColorYOffset
// and the offset here in scrollViewDidScroll to cover up or down movements
// say 1 cell's height, then set your isBackgroundColorRed to true
}
}
Try adding this to your project and update yur bridging header.
UIColor-CrossFade
This technique will give you a nice UX rather than a sudden background change.

CellForRow is a way to detect scrolling which also gives you the indexPath.row of the next cell entering the view. If the user scrolls such a short distance that a new or recycled cell is not even configured, then dragging will not be detected. However even with built in methods, such as scrollViewWillBeginDragging, short distance scrolling will not be detected.
var lastCellForRowAtIndex = 0
var isScrollingUp = false
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
isScrollingUp = indexPath.row > lastCellForRowAtIndex
lastCellForRowAtIndex = indexPath.row
}

cellForRowAtIndexPath will be repeatedly called when you are scrolling the tableView unless you have very few number of rows.

Related

Swift. How to scroll a Scrollview (TableView) first to move it to full screen, and then scroll the content itself

I noticed in some applications the interesting behavior of the scroll. For example, in the Viber application, there is some kind of header view in the “More” menu at the top, below, as it seems to me, is a uitableview table. When scrolling down, the top header is first hidden and the table is aligned to full screen and only then the content is scrolled in it:
Moreover, this scroll happens smoothly along with scrolling, even if you swipe a little with your finger and release it, the scroll will continue to move with the same inertia and hide the upper header view smoothly and then scroll through the table itself. How to achieve the same effect?
I tried to do the same using scrollViewDidScroll, but it turned out to do only the following:
At the beginning of scrolling, the table itself scrolls and at the same time, the header view is also hidden, and an increased scroll speed is obtained. Maybe you can somehow do it so that at first the scrolling in the table goes, but the content in it actually remains in one place. Also, when the tableview is scrolled to the very top, then if you scroll it even further, then I would like it to move itself, and not the content in it
My code is following:
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var tableViewTopAnchor: NSLayoutConstraint!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let y = scrollView.contentOffset.y
if y <= headerViewHeight && y >= 0 {
tableViewTopAnchor.constant = -y
} else if tableViewTopAnchor.constant != -headerViewHeight && y > headerViewHeight {
tableViewTopAnchor.constant = -headerViewHeight
}
}
let headerViewHeight: CGFloat = 150
Storyboard:
I tried to use scrollView.contentOffset.y = 0 to stop scrolling content in a table in scrollViewDidScroll, but then scrolling generally stops working (
I am sorry if such question already exists, but I couldn't find exactly the same. I would appreciate a link and/or example.
Thanks in advance for your help :)

Reset Scrollview in UICollectionViewCell after scrolling

I'm creating a photo gallery with this concept. For listing the images, I've used UICollectionView. Each image is stored in full screen size custom cells. Inside the cell, I have UIScrollView and inside it I have UIImageView. ScrollView is used to zoom the image. All working fine but, when I zoom in one image and scroll to another cell without zooming out, I want that previous cell's scrollview to be reset.
Inside custom cell class, I set ZoomScale for each cell's scrollview when they are initiated like so:
func configureCell(_ photo: String){
albumPhoto.image = UIImage(named: photo)
scrollView.setZoomScale(1.0, animated: false)
}
I configure each cell in cellForItemAt function before returning the cell.
When I zoom in in the first cell, scroll to second and scroll back to the first cell, the image is still zoomed in. But if I scroll to second and third cell and then return to first cell, the image is zoomed out to default.
How can I achieve that even after scrolling immediately back from second cell to first, the scroll view will be set to default zoom scale.
you need to implement this method: https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618087-collectionview
and zoom out the scrollView before displaying the cell. This method will handle your case. Happy coding ;)
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
First make your class conforms to
class profViewController: UIScrollViewDelegate
As UICollectionView is a subclass of UIScrollView
Second implement this delegate method
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let cd = areaSettTable.visibleCells as!
[profTableViewCell]
//////
loop here
}
then loop through this array and call the function that resets the zoom for every cell
note : you should change profTableViewCell name to your cell

Scrolling bad practice for UICollectionViews

I made a horizontal scrolling UICollectionView and I want the cell that's in the middle to have a white font while the rest is black.
If I just use scrollViewDidEndDecelerating highlighting of the middle cell seems to jump around more than if I use both scrollViewWillBeginDecelerating and scrollViewDidEndDecelerating to highlight the middle cell. Is this bad practice?
extension CurrencySelectorTableViewCell: UIScrollViewDelegate{
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
self.findCenterIndex()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.findCenterIndex()
}
}
This code btw still doesn't animate perfectly like I want it to so I'm open to any advice how to make this scrolling mechanism as smooth as possible.
When the UICollectionView thus starts scrolling this function is triggered:
func findCenterIndex() {
let center = self.convert(self.collectionView.center, to: self.collectionView)
let index = collectionView!.indexPathForItem(at: center)
if let selectedIndex = index {
self.selectedCell = selectedIndex.item
self.collectionView.reloadData()
}
}
Upon reloading the UICollectionView the label in the cell that is located in the middle will look different from the rest:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CurrencySelectorCollectionViewCell", for: indexPath) as! CurrencySelectorCollectionViewCell
if (indexPath.item == self.selectedCell) {
cell.currencyLabel.textColor = UIColor.white
cell.currencyLabel.font = cell.currencyLabel.font.withSize(22)
} else {
cell.currencyLabel.textColor = UIColor.black
cell.currencyLabel.font = cell.currencyLabel.font.withSize(15)
}
cell.currencyLabel.text = currencies[indexPath.item]
return cell
}
Right now it jumps around a little bit because it will only change the label when the scrolling has just started or just stopped. I would like this effect on the UITextLabel to happen continuously throughout the scrolling process.
Try to add removeAllAnimations() of your UILabel layer before you fire off a new animation:
[view.layer removeAllAnimations];
EDIT:
Based on your edit in the question, you are not running any animation. You are calling reloadData on your UICollectionView, which is really bad practice.
You should just simple either:
1: (Bad option)
Reload the Cell only with performBatchUpdates(_:completion:)
2: Good option
Access the cell as a variable in your cell findCenterIndexwith cellForItem(at:) and simply just do your update to the label.
You can also deselect the other cells by getting an array of the visibleCells and simply just do as same described above, but you fire your "deselection" code instead. You could actually do this before you run your selection code. Or do everything in one action by Simply run a for loop on the visible cells and "deselect" them in your loop, and select the one in your CGPoint center.
This way, you never even have to reload your UICollectionView and is the best practice. And you also avoid flickers and animations.

Settings UITableViewCell Heights With Multiline UIButtons in Auto Layout

I have a UITableView with custom UITableViewCells, each has a UIButton inside. I'm setting buttons' titles from an array, so the size of the buttons change according to the title. I need to return correct height based on the inner button's size in heightForRowAtIndexPath event.
Since I'm using auto layout, I've created an outlet for the button's height constraint and I'm updating it in the cell's layoutSubviews() event like this:
class CustomCell: UITableViewCell {
/* ... */
override func layoutSubviews() {
super.layoutSubviews()
self.myButton?.layoutIfNeeded()
self.heightConstraint?.constant = self.myButton!.titleLabel!.frame.size.height
}
}
Then I return the height based on the button height and top-bottom margins like so:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
var cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomCell
cell.myButton?.setTitle(self.data[indexPath.row], forState: UIControlState.Normal)
cell.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds))
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell.myButton!.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + (cell.topMarginConstraint!.constant * 2) /* top-bottom margins */ + 1 /* separator height */
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomCell
cell.myButton?.setTitle(self.data[indexPath.row], forState: UIControlState.Normal)
return cell
}
On the first launch, there seems to be no problem. However, after I begin scrolling, then the height of some rows seem to be mistaken. When I get back to the top, I see that previous cell heights get to be broken as well.
When I googled for similar problems, issue seems to be about reusable cells, though I was unable to find another way to calculate the height. What can be done to reuse cells correctly or getting the correct height, perhaps by another method?
More info and source code:
Constraints set by IB like this:
Here's the cells on the first launch:
After some scrolling:
Full code of the project can be found on Github.
According to this
Configure tableView as
func configureTableView() {
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 160.0
}
Call it on your viewDidLoad method
Than configure your uibutton height constraint to be greater then or equal.
Override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat where you can place your estimation height code
First off, it's better if you perform constraint updates in func updateConstraints() method of UIView. So instead of
override func layoutSubviews() {
super.layoutSubviews()
self.myButton?.layoutIfNeeded()
self.heightConstraint?.constant = self.myButton!.titleLabel!.frame.size.height
}
I would do
override func updateConstraints() {
self.myButton?.layoutIfNeeded()
self.heightConstraint?.constant = self.myButton!.titleLabel!.frame.size.height
super.updateConstraints()
}
Note that you should call the super implementation at the end, not at the start. Then you would call cell.setNeedsUpdateConstraints() to trigger a constraint update pass.
Also you should never directly manipulate the cell bounds the way you are doing in heightForRowAtIndePath: method, and even if you are completely sure that manipulating directly is what you want, you should manipulate cell.contentView's bounds, not the cell's bounds. If you are looking to adjust the cell height dynamically with respect to the dimensions of the content, you should use self sizing cells. If you need to support iOS 7, then this answer tells you how to achieve that behaviour with autolayout only (without touching the bounds etc).
To reiterate the answer, you should do:
func viewDidLoad() {
self.dummyCell = CustomCell.init()
// additional setup
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
self.dummyCell.myButton?.setTitle(self.data[indexPath.row], forState: UIControlState.Normal)
self.dummyCell.layoutIfNeeded() // or self.dummyCell.setNeedsUpdateConstraints() if and only if the button text is changing in the cell
return self.dummyCell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
Please know that the answer I linked to outlines a strategy to get the cell height via autolayout, so only writing the code changes I proposed won't be enough unless you set your constraints in a way that makes this solution work. Please refer to that answer for more information.
Hope it helps!
First of all, remove the height constraint of button and bind it to top and bottom with cell.
Then, in your cell' height, calculate height of the text based on the width and font of button. This will make the cell's height dynamic and you wont need height constraint anymore.
Refer the link below to get the height of text:
Adjust UILabel height to text
Hope it helps. If you need help further or understanding anything, let me know.. :)

Reordering UITableView without reorder control

I need the user to be able to reorder a UITableView by this way: he touches a cell for a predetermined period (e.g. 1 second), then he can drag and drop it over the other cells.
I know how to implement the 'long touch' detection using a gesture recognizer, but what is the best way to implement the drag and drop ability without using a reorder control (the user should drag the cell from anywhere in the cell, not only from the reorder control)?
This is an old question, but here's a solution that's tested and working with iOS 8 through 11.
In your UITableViewCell subclass try this:
class MyTableViewCell: UITableViewCell {
weak var reorderControl: UIView?
override func layoutSubviews() {
super.layoutSubviews()
// Make the cell's `contentView` as big as the entire cell.
contentView.frame = bounds
// Make the reorder control as big as the entire cell
// so you can drag from everywhere inside the cell.
reorderControl?.frame = bounds
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: false)
if !editing || reorderControl != nil {
return
}
// Find the reorder control in the cell's subviews.
for view in subviews {
let className = String(describing: type(of:view))
if className == "UITableViewCellReorderControl" {
// Remove its subviews so that they don't mess up
// your own content's appearance.
for subview in view.subviews {
subview.removeFromSuperview()
}
// Keep a weak reference to it for `layoutSubviews()`.
reorderControl = view
break
}
}
}
}
It's close to Senseful's first suggestion but the article he references no longer seems to work.
What you do, is make the reorder control and the cell's content view as big as the whole cell when it's being edited. That way you can drag from anywhere within the cell and your content takes up the entire space, as if the cell was not being edited at all.
The most important downside to this, is that you are altering the system's cell view-structure and referencing a private class (UITableViewCellReorderControl). It seems to be working properly for all latest iOS versions, but you have to make sure it's still valid every time a new OS comes out.
I solved the question of the following steps:
Attach gesture recognizer to UITableView.
Detect which cell was tapped by "long touch". At this moment create a snapshot of selected cell, put it to UIImageView and place it on the UITableView. UIImageView's coordinates should math selected cell relative to UITableView (snapshot of selected cell should overlay selected cell).
Store index of selected cell, delete selected cell and reload UITableView.
Disable scrolling for UITableView. Now you need to change frame of snapshot UIImageView when you will drag cell. You can do it in touchesMoved method.
Create new cell and reload UITableView (you already have stored index) when the user finger leaves screen.
Remove the snapshot UIImageView.
But it was not easy to do it.
The article Reordering a UITableViewCell from any touch point discusses this exact scenario.
Essentially you do the following:
Find the UITableViewCellReorderControl (a private class).
Expand it so it spans the entire cell.
Hide it.
The user will now be able to drag the cell from anywhere.
Another solution, Cookbook: Moving Table View Cells with a Long Press Gesture, achieves the same effect by doing the following:
Add a long press gesture recognizer on the table view.
Create a snapshot of the cell when the cell is dragged.
As the cell is dragged, move the snapshot around, and call the -[UITableView moveRowAtIndexPath:toIndexPath:].
When the gesture ends, hide the cell snapshot.
For future reference...
I had the same problem, I found another question(Swift - Drag And Drop TableViewCell with Long Gesture Recognizer) about it and someone suggested this tutorial: https://www.freshconsulting.com/create-drag-and-drop-uitableview-swift/
worked just perfectly for me
I know this is a question about UITableView. But I ended with a solution of using UICollectionView rather than UITableView to implement longtap reorder. Its easy and simple.
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { }
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession,
at: indexPath: IndexPath) -> [UIDragItem] {
return [UIDragItem(itemProvider: NSItemProvider())]
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session:
UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if session.localDragSession != nil {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UITableViewDropProposal(operation: .cancel, intent: .unspecified)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}

Resources