Custom table view scrolling animation - ios

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

Related

Slide view together with UICollectionView cell

i have this structure. Scrollview -> UICollectionview + Label
This viewcontroller has array of items (BlockItem). On scrollViewDidScroll i change current test label (description)
func scrollViewDidScroll(_ scrollView:UIScrollView)
{
let midX:CGFloat = scrollView.bounds.midX
let midY:CGFloat = scrollView.bounds.midY
let point:CGPoint = CGPoint(x:midX, y:midY)
guard
let indexPath:IndexPath = collectionView.indexPathForItem(at:point)
else
{
return
}
let currentPage:Int = indexPath.item
if let found = items.first(where: {$0.id == String(block[currentPage])}) {
description.text = found.description
}
}
The main issue that i want my description label will be moving together when i move my collectionview cell and appear from the next cell. In other words, if I scroll to the left my description should move along with the cell, i.e. go to the left and appear on the right of the screen.
How can i do this? I know i could make a big collectionview but i need that only image part should be scrollable, not the entire block.
Here is what i want to achieve
Here is video example: scroll works only if i swipe on the image area, and doesn't work when i scroll down the page
https://drive.google.com/file/d/1kl1GYgXvK4bL3toTfOvpxF2WqS56pQO9/view?usp=sharing
You exactly said this:
"The main issue that I want my description label will be moving together when I move my collection view cell and appear from the next cell."
If you want your description label moving together, just include them into your collection cell. It's very clear from your point there
I don't understand why u need to separate it, but you want it to slide from new cell.
If you insisted that you want to archive the label, not in a scroll view, then use 2 label
One label for this cell and one label will come from scroll direction, u can archive this by creating manually. This is a simple code for a swipe left to scroll
UIView.animate(withDuration: 0.5) {
//insert your label animation
//current label moved with the scroll to left
//create a new label from the outer right of the current view, scroll it to the middle of your view
}
But it will be hard work for good animation or scroll support.

UICollectionView cellForItemAt: won't be called while layoutAttributesForElements are returning values

I have a bit complex UICollectionView with my custom UICollectionViewLayout.
I added a pinch gesture behavior onto it and adjust the layout according to that result.
It's basically working pretty well, the cells are resized and repositioned properly in normal cases.
But when it comes to a particular condition the cells disappear and never appear again.
So far I'm unable to clarify the condition, but it happens often when you pinched the view to a smaller size.
The issue is, layoutAttributesForElements in my collection view layout is called (of course this also implies that numberOfCells:inSection is called as well) and it's returning reasonable cell geometry, but actual cell generation (cellForItemAt:) won't be called.
Before jumping into the code(as it's a bit too long and complicated) I want to ask you guys if any of you have had the same kind of situation.
Below is the summary of what's happening and what I see so far.
It's not that always happening. It happens only after pinching and reached to a certain condition. So this is not a basic kind of how-to-use-UICollectionView issue.
Even when it happens layoutAttributesForElements keeps being called (as you keep pinching)
The layout attributes don't have crazy values like zero size or position of far out of view range. They all have the position attributes that fit into the collection view's content size.
Collection view claims the proper content view size (at least as reported on the debugger.)
When it happens you cannot see any cells in the view hierarchy in the View Debugger. Meaning, it's not zero sized or clear colored but cells themselves are gone. This corresponds to the fact that cellForItemAt: is not called.
After it happened you cannot see the cells anymore even you pinch the screen back to the original scale.
Any information is appreciated. Thanks!
EDIT
My collection view layout is like this. This project is a musical sequencer and the collection view is showing your musical notes in piano roll style.
//
// YMPianoRollLayout.swift
//
import UIKit
class YMPianoRollLayout: UICollectionViewLayout {
let notes : Array<CGPoint> = []
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!;
}
override var collectionViewContentSize : CGSize {
let cv = self.collectionView as! YMPianoRollCollectionView
let screenInfo = cv.pianoRollViewController.screenInfo
let totalSize = screenInfo.collectionViewContentsSize();
print("contentSize = \(totalSize)")
return totalSize;
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let cv = self.collectionView as! YMPianoRollCollectionView;
let pianoRoll = cv.pianoRollViewController;
// Check which musical note can be included in the view rect
let indexArray: Array<Int> = pianoRoll!.getNoteIndexes(inRect:rect, useOnlyStartTime: false);
var retArray : [UICollectionViewLayoutAttributes] = []
for i in indexArray {
if let _ = pianoRoll?.pattern.eventSequence[i] as? YMPatternEventNoteOn {
retArray.append( self.layoutAttributesForPatternEventInfo(i) )
}
}
// This always reports non-zero count. Also checked the positions of each array members
// by breaking here and they all had proper size and positions
print("retArray count = \(retArray.count)");
return retArray
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let i = Int((indexPath as NSIndexPath).row)
return self.layoutAttributesForPatternEventInfo(i)
}
//
// This is my specific func to convert the musical time-pitch into the view geometory
//
func layoutAttributesForPatternEventInfo(_ index: Int) -> UICollectionViewLayoutAttributes!{
let cv = self.collectionView as! YMPianoRollCollectionView
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let screenInfo = cv.pianoRollViewController.screenInfo
// Retrieve musical event
let event = cv.pianoRollViewController.pattern.eventSequence[index]
let index = IndexPath(row:index, section: 0)
let newAttr = UICollectionViewLayoutAttributes(forCellWith:index)
var frame : CGRect!
if let e = event as? YMPatternEventNoteOn {
let noteNo = e.noteNo;
let origin = YMMusicalValuePoint(time:event.time, noteNumber:noteNo);
let size = YMMusicalValueSize(timeLength:e.duration, numberOfNotes: 1);
// Actual size calculation is done in my "screenInfo" class.
frame = screenInfo.getPhysicalRange(YMMusicalValueRange(origin:origin, size:size));
} else {
frame = CGRect(x: 0, y: 0, width: 0, height: 0);
}
newAttr.frame = frame;
newAttr.zIndex = 1;
return newAttr
}
//
// For checking the bounds
//
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
print("BBB newBounds = \(newBounds)")
return true
}
}
Sounds like an interesting problem...
More info on your custom UICollectionViewLayout subclass (i.e. the code) would be useful but I can offer an overview of how UICollectionView typically updates visible content in response to bounds changes (i.e. content offset, rotation) and invalidateLayout calls.
When invalidateLayout fires the internal attributes cache is cleared signaling the need to ask the layout on the next layoutSubviews pass what should be visible. Conversely, if the layout is not invalidated and the attributes are known in the cache the layout will not be asked.
The magic all happens when the current CA transaction commits and UICollectionView's layoutSubviews is invoked. At this point, a diff between what was last known to be on screen and what should now be on screen is computed according the current "visible bounds" (effectively the scroll view's bounds which includes the offset into the layout).
Cells no longer visible in the new visible bounds will be returned to the reuse queue and newly appearing cells (as per the layout) will be constructed and existing items (that have changed) will be updated with their new attributes.
Based on your description, it sounds like when the next layoutSubviews fires the queried attributes (possibly from the cache!) aren't returning anything for the new visible bounds therefore nothing "new" appears -- along with the existing items disappearing...
A few things to investigate might include:
Is your gesture causing your custom layout to invalidate? If not, it probably should so the UICollectionView knows to not trust it's internal attributes cache and always ask the layout for new attributes during the "diffing" process.
How is the bounds of the collection changing during the pinch gesture?
This directly affects the diff since it will use this visible bounds to determine what should be displayed next.
How does your layout respond to shouldInvalidateForBoundsChange:?
Most layouts only invalidate when the extents (e.g. rotation) change, so the UICollectionView will normally rely on it's cached attributes when performing the diff. In your case if you are tweaking the bounds during the gesture, you'll want to unconditionally return YES here.
When you get into this wonky state, you might try pausing into the debugger e.g.
po [[cv collectionViewLayout] invalidateLayout]
po [cv setNeedsLayout]
po [cv layoutIfNeeded]
...and resume. Has everything re-appeared?
If so, it sounds like the layout isn't being invalidated under certain circumstances and the attributes being returned are indeed reasonable.
If not, ping the layout and see what if it is reporting as visible is reasonable via:
po [[cv collectionViewLayout] layoutAttributesForElementsInRect:cv.bounds]

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 :)

Swift table view with few rows can be scrolled up

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.

Limiting vertical movement of UIAttachmentBehavior inside a UICollectionView

I have a horizontal UICollectionView with a custom UICollectionViewFlowLayout that has a UIAttachmentBehavior set on each cell to give it a bouncy feel when scrolling left and right. The behavior has the following properties:
attachmentBehavior.length = 1.0f;
attachmentBehavior.damping = 0.5f;
attachmentBehavior.frequency = 1.9f;
When a new cell is added to the collection view it's added at the bottom and then animated to its position also using a UIAttachmentBehavior. Naturally it bounces up and down a bit till it rests in its position. Everything is working as expected till now.
The problem I have starts appearing when the collection view is scrolled left or right before the newly added cell has come to rest. The adds left and right bounciness to the up and down one the cell already has from being added. This results in a very weird circular motion in the cell.
My question is, is it possible to stop the vertical motion of a UIAttachmentBehavior while the collection view is being scrolled? I've tried different approaches like using multiple attachment behaviors and disabling scrolling in the collection view till the newly added cell has come to rest, but non of them seem to stop this.
One way to solve this is to use the inherited .action property of the attachment behavior.
You will need to set up a couple of variables first, something like (going from memory, untested code):
BOOL limitVerticalMovement = TRUE;
CGFloat staticCenterY = CGRectGetHeight(self.collectionView.frame) / 2;
Set these as properties of your custom UICollectionViewFlowLayout
When you create your attachment behavior:
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
attachment.damping = 1.0f;
attachment.frequency = 1.5f;
attachment.action = ^{
if (!limitVerticalMovement) return;
CGPoint center = item.center;
center.y = staticCenterY;
item.center = center;
};
Then you can turn the limiting function on and off by setting limitVerticalMovement as appropriate.
Have you tried manually removing animations from cells with CALayer's removeAllAnimations?
You'll want to remove the behaviour when the collection view starts scrolling, or perhaps greatly reduce the springiness so that it comes to rest smoothly, but quickly. If you think about it, what you're seeing is a realistic movement for the attachment behaviour you've described.
To keep the vertical bouncing at the same rate but prevent horizontal bouncing, you'd need to add other behaviours - like a collision behaviour with boundaries to the left and right of each added cell. This is going to increase the complexity of the physics a little, and may affect scrolling performance, but it would be worth a try.
Here's how I managed to do it.
The FloatRange limits the range of the attachment, so if you want it to go all the way up and down the screen you just set really large numbers.
This goes inside func recognizePanGesture(sender: UIPanGestureRecognizer) {}
let location = sender.location(in: yourView.superview)
var direction = "Y"
var center = CGPoint(x: 0, y: 0)
if self.direction == "Y" {center.y = 1}
if self.direction == "X" {center.x = 1}
let sliding = UIAttachmentBehavior.slidingAttachment(with: youView, attachmentAnchor: location, axisOfTranslation: CGVector(dx: center.x, dy: center.y))
sliding.attachmentRange = UIFloatRange(minimum: -2000, maximum: 2000)
animator = UIDynamicAnimator(referenceView: self.superview!)
animator.addBehavior(sliding)
If you're using iOS 9 and above then sliding function within attachment class will work perfectly for that job:
class func slidingAttachmentWithItem(_ item: UIDynamicItem,
attachmentAnchor point: CGPoint,
axisOfTranslation axis: CGVector) -> Self
it can be used easily, and it's very effective for sliding Apple documentation
I've resorted to disabling scrolling in the collection view for a specific amount of time after a new cell is added, then removing the attachment behavior after that time has passed using its action property, then adding a new attachment behavior again immediately.
That way I make sure the upwards animation stops before the collection view is scrolled left or right, but also the left/right bounciness is still there when scrolling.
Certainly not the most elegant solution but it works.

Resources