Limiting vertical movement of UIAttachmentBehavior inside a UICollectionView - ios

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.

Related

Best way to add multiple diagonal connection lines between TableViewCells

I'm creating an app that needs to show a tableview like below image
Similar colored circles are to be matched with a line.
Which view i can add the lines?
Or need to create a new view above tableview? But still my tableview needs to be scrolled.
How can i achieve this?
Update for Bounty
I want to implement the same with incliend lines between neighbouring circles. How to achieve the same?
Demonstration below:
create design like this
Based on your requirement just hide upper line and lower line of circle
You need to create collection view in tableview cell. In collection view you create one cell. Design the same user interface like your design. Show and hide the view with matching of rule. It will not affect tableview scrolling. and with this approach you can also provide scroll in collection view cell. i can provide you coded solution if you able to provide me more information. Thanks
You can use this Third Party LIb
You need to use a combination of collection view and a table view to give support for all devices.
1.Create one collection view cell with following layout
Hide upper and lower lines as per your need
Add collection view in table view cell and managed a number of cells in collection view depending upon the current device width and item's in between spacing.
You can create a vertical label without text, set the background color with black and place it behind the circle in view hierarchy and set a width of the label as per your requirement. Then you can hide unhide the label whenever you want.
P.S.: Make sure to hide your cell separator.
I have created a demo project. You can find it here. I tried to match your requirements. You can update the collection view settings to handle the hide and show of labels.
Let me know if you have any questions.
Hope this help.
Thanks!
To connect any circle with any other circle in the cell above / below, it will be easier and cleaner to create the connection lines dynamically rather than building them into the asset as before. This part is simple. The question now is where to add them.
You could have the connection lines between every two cells be contained in the top or bottom cell of each pair, since views can show content beyond their bounds.
There's a problem with this though, regardless of which cell contains the lines. For example, if the top cell contains them, then as soon as it is scrolled up off screen, the lines will disappear when didEndDisplayingCell is called, even though the bottom cell is still completely on screen. And then scrolling slightly such that cellForRow is called, the lines will suddenly appear again.
If you want to avoid that problem, then here is one approach:
One Approach
Give your table view and cells a clear background color, and have another table view underneath to display a new cell which will contain the connection lines.
So you now have a background TVC, with a back cell, and a foreground TVC with a fore cell. You add these TVC's as children in a parent view controller (of which you can set whatever background color you like), disable user interaction on the background TVC, and peg the background TVC's content offset to the foreground TVC's content offset in an observation block, so they will stay in sync when scrolling. I've done this before; it works well. Use the same row height, and give the background TVC a top inset of half the row height.
We can make the connection lines in the back cell hug the top and bottom edges of the cell. This way circles will be connected at their centre.
Perhaps define a method in your model that calculates what connections there are, and returns them, making that a model concern.
extension Array where Element == MyModel {
/**
A connection is a (Int, Int).
(0, 0) means the 0th circle in element i is connected to the 0th circle in element j
For each pair of elements i, j, there is an array of such connections, called a mesh.
Returns n - 1 meshes.
*/
func getMeshes() -> [[(Int, Int)]] {
// Your code here
}
}
Then in your parent VC, do something like this:
class Parent_VC: UIViewController {
var observation: NSKeyValueObservation!
var b: Background_TVC!
override func viewDidLoad() {
super.viewDidLoad()
let b = Background_TVC(model.getMeshes())
let f = Foreground_TVC(model)
for each in [b, f] {
self.addChild(each)
each.view.frame = self.view.bounds
self.view.addSubview(each.view)
each.didMove(toParent: self)
}
let insets = UIEdgeInsets(top: b.tableView.rowHeight / 2, left: 0, bottom: 0, right: 0)
b.tableView.isUserInteractionEnabled = false
b.tableView.contentInset = insets
self.b = b
self.observation = f.tableView.observe(\.contentOffset, options: [.new]) { (_, change) in
let y = change.newValue!.y
self.b.tableView.contentOffset.y = y // + or - half the row height
}
}
}
Then of course there's your drawing code. You could make it a method of your back cell class (a custom cell), which will take in a mesh data structure and then draw the lines that represent it. Something like this:
class Back_Cell: UITableViewCell {
/**
Returns an image with all the connection lines drawn for the given mesh.
*/
func createMeshImage(for mesh: [(Int, Int)]) -> UIImage {
let canvasSize = self.contentView.bounds.size
// Create a new canvas
UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0)
// Grab that canvas
let canvas = UIGraphicsGetCurrentContext()!
let spacing: CGFloat = 10.0 // whatever the spacing between your circles is
// Draw the lines
for each in mesh {
canvas.move(to: CGPoint(x: CGFloat(each.0) * spacing, y: 0))
canvas.addLine(to: CGPoint(x: CGFloat(each.1) * spacing, y: self.contentView.bounds.height))
}
canvas.setStrokeColor(UIColor.black.cgColor)
canvas.setLineWidth(3)
canvas.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
You'd probably want to create a Mesh class and store the images in that model, to avoid redrawing.

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

Zoom in/out on swiping tableView Animation

I want to achieve this animation:
Here is the Gif: http://imgur.com/4TZIbwp
Since the content is dynamic, I'm using tableView to populate the data.
I've tried scrollViewDidScroll delegate methods to change the constraints but it's not helping me. I've even tried swipe gesture, but still can't manage to achieve this.
Can anyone provide a knowledge, a bit of code for getting this animation.
I have tried to tackle your issue in this project.
The flaw with this solution is that it requires an unattractive inset at the top of the table view to translate the offset into a meaningful variable with which to shrink the table view.
The relevant code in the project is within the scroll delegate function:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = min(0.0, max(-maximumOffset, scrollView.contentOffset.y))
let constant = -yOffset
topTableViewConstraint.constant = constant
leadingTableViewConstraint.constant = constant / 5.0
trailingTableViewConstraint.constant = constant / 5.0
view.layoutIfNeeded()
}
I am sorry that I cannot be more helpful, or provide you with a final solution.
Hopefully the project will aid you in find that answer.

UIScrollViewDelegate - animateWithDuration in scrollViewDidEndDragging is not working as expected

I have a UIScrollView with 2 sub views/pages side by side (horizontal content size = 2 * Screen Width + gutter space between pages). I would like to increase the completion speed of the animation, ie after the user has completed the dragging and lifted the finger. Based on the suggestions found in SO, I implemented a UIScrollViewDelegate as below.
class MyScrollViewDelegate: NSObject, UIScrollViewDelegate
{
var targetX: CGFloat = 0.0
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
// I have not implemented the paging logic yet as I wanted to test the replacement animation first.
// Below code simply uses the suggested tagetOffset, but limiting the same between the valid min and max values
targetX = fmax(0, fmin(scrollView.contentSize.width - scrollView.frame.size.width, targetContentOffset.memory.x))
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
UIView.animateWithDuration(0.25, animations: { scrollView.contentOffset = CGPointMake(self.targetX, 0) })
}
}
My understanding is that once the dragging is over, my own animation code in the scrollViewDidEndDragging will take over and complete the translation. The problem I am facing is that there seems to be an additional animation/jump when I pull the view inward when it is at the leftmost or rightmost edge.
For example, when the contentOffset.x = 0 (left edge), if I pull the view rightward by say 20 points and release (even after pausing for a second), the contentOffset.x & targetContentOffset.memory.x would be -20, and the calculated self.targetX would be 0. Hence in my animation I expect the page to move back towards left by 20 points with given speed as soon as I lift the finger. But what I observe is that, the page goes further to the right by almost the same amount (dragged distance) and then animates back from there to 0. The movement is so fast that I can't make out whether it is an animation or direct jump. Afterwards it follows my animation parameters to fall back.
As mentioned, the rightward jump seems to be proportional to the dragging distance, suggesting that my assumption about the "current position" before the start of animation is probably wrong. But I am not able to figure out the exact reason. Your help is much appreciated. The setup is ios 9.0, swift 2.0, xcode 7.0.1.
Edit 1:
I commented the animateWithDuration (but kept the scrollViewDidEndDragging). Animations stopped except in the edge region, where there is a default animation pulling the content back. Further reading pointed to the bounce property. I have a doubt that this default animation is colliding with the one I supplied.
Edit 2:
The bounce animation seems to be the culprit. While searching in this direction I came across another question in SO (Cancel UIScrollView bounce after dragging) describing the issue and possible solutions.
The issue seems to be because of queuing up of bounce animation and the custom animation. For details please read - Cancel UIScrollView bounce after dragging. I am not sure how the chosen answer solves the problem of custom duration. There is another solution involving sub-classing. That also didn't work in my case. Not sure whether it is due to different iOS versions.
Following is what worked for me. It is only the basic snippet. You can enhance the same to have better animation curves and velocity handling.
func scrollViewWillBeginDecelerating(scrollView: UIScrollView)
{
// below line seems to prevent the insertion of bounce animation
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
// provide your animation with custom options
UIView.animateWithDuration(0.25, animations: { scrollView.contentOffset = CGPointMake(targetX, targetY) })
}

How to trace which image is currently seen in my Uiscrollview?

I am new to iOS.
I am working on UIScrollView based application. I have 3 images in UIScrollView.
I have 2 UIButtons which have selector method for Facebook and Twitter sharing.
I want to share image which is currently seen in UIScrollView to Facebook & Twitter.
I am stuck at extracting image at current scroll point.
After Googling I think something like UIPageController is the solution for me.
But I do not have any idea about how to implement it.
Please help me sort it out. Thank You!
I would say you have multiple options here, this is really a design question.
An easy (but also not very elegant) approach would be to fetch the current offset of the scroll view using its contentOffset property (depending on whether your scroll view scrolls horizontically or vertically, you have ease to use the x or y value of the contentOffset).
Next, you get the position values from your image views (again either myImageView.frame.origin.x or myImageView.frame.origin.y) and you can then calculate which of the image views is currently in the offset of the scroll view.
You can use a page controller (basically, you allow the user to end its scrolling on specific index).
Or when the user touches a button, you can retrieve the contentOffset of the scrollView and compare this value with the origin of the frame your images.
Assume, you created your images with these frames:
image1.frame = CGRectMake(0,0,320,250);
image2.frame = CGRectMake(0,250,320,250);
image3.frame = CGRectMake(0,500,320,250);
Now when you get the action:
-(IBAction)share:(id)sender {
CGPoint offset = self.scrollView.contentOffset;
if (offset.y < 250) {
//First image selected
}else if (offset.y > 500) {
//Third image selected
} else {
//Second image selected
}
}

Resources