Circular wheel animation implementation in ios - ios

I have to do a animation like :
https://www.flickr.com/photos/134104584#N07/20409116003/in/datetaken/
Currently I am trying to achieve this using custom collection view layout and subclassing UICollectionViewLayout.
Any help or way to achieve this ?

I had the same task and that's what I've found:
You can make your own solution, following great tutorial made by Rounak Jain. The tutorial is available here:
https://www.raywenderlich.com/107687/uicollectionview-custom-layout-tutorial-spinning-wheel
You can reuse the existing code.
Because of time limits I've chosen the second variant, and the best repo I've found was:
https://github.com/sarn/ARNRouletteWheelView
The usage is pretty straightforward, but there are two tips that can be useful:
scrollToItemAtIndexPath was not working and was crashing the app. The Pull Request to fix it: https://github.com/sarn/ARNRouletteWheelView/pull/3/commits/7056e4bac2a04f2c8208d6d43e25d62a848f032d
If you want to make items smaller and disable rotation of the item itself (see gif), you can do the following in your UICollectionViewCell subclass:
#import "ARNRouletteWheelCellScaledAndRotated.h"
#import <ARNRouletteWheelView/ARNRouletteWheelLayoutAttributes.h>
#implementation ARNRouletteWheelCellScaledAndRotated
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
if(layoutAttributes != nil && [layoutAttributes isKindOfClass:[ARNRouletteWheelLayoutAttributes class]]) {
ARNRouletteWheelLayoutAttributes *rouletteWheelLayoutAttributes = (ARNRouletteWheelLayoutAttributes *)layoutAttributes;
self.layer.anchorPoint = rouletteWheelLayoutAttributes.anchorPoint;
// update the Y center to reflect the anchor point
CGPoint center = CGPointMake(self.center.x, self.center.y + ((rouletteWheelLayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)));
self.center = center;
// rotate back
CGAffineTransform rotate = CGAffineTransformMakeRotation(-rouletteWheelLayoutAttributes.angle);
// scale
CGFloat scale = 1 - fabs(rouletteWheelLayoutAttributes.angle);
CGAffineTransform translate = CGAffineTransformMakeScale(scale, scale);
// Apply them to a view
self.imageView.transform = CGAffineTransformConcat(translate, rotate);
}
}
#end

Update this tutorial - https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel. Old version not work. This version good work for Swift 5 + i add pageControl if somebody need.
import UIKit
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
//1
var anchorPoint = CGPoint(x: 0.5, y: 0.5)
var angle: CGFloat = 0 {
//2
didSet {
zIndex = Int(angle * 1000000)
transform = CGAffineTransform.identity.rotated(by: angle)
}
}
override func copy(with zone: NSZone? = nil) -> Any {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copy(with: zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
protocol CircularViewLayoutDelegate: AnyObject {
func getCurrentIndex(value: Int)
}
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 214, height: 339)
let spaceBeetweenItems: CGFloat = 50
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItems(inSection: 0) > 0 ?
-CGFloat(collectionView!.numberOfItems(inSection: 0)) * anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize.width - itemSize.width)
}
var radius: CGFloat = 1000 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return atan((itemSize.width + spaceBeetweenItems) / radius)
}
weak var delegate: CircularViewLayoutDelegate?
override var collectionViewContentSize: CGSize {
return CGSize(width: (CGFloat(collectionView!.numberOfItems(inSection: 0)) * itemSize.width) + 100, height: collectionView!.bounds.height)
}
override class var layoutAttributesClass: AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2.0
let anchorPointY = (itemSize.height + radius) / itemSize.height
// Add to get current cell index
let theta = atan2(collectionView.bounds.width / 2.0,
radius + (itemSize.height / 2.0) - collectionView.bounds.height / 2.0)
let endIndex = collectionView.numberOfItems(inSection: 0)
let currentIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))
delegate?.getCurrentIndex(value: currentIndex)
attributesList = (0..<collectionView.numberOfItems(inSection: 0)).map {
(i) -> CircularCollectionViewLayoutAttributes in
//1
let attributes = CircularCollectionViewLayoutAttributes.init(forCellWith: IndexPath(item: i, section: 0))
attributes.size = self.itemSize
//2
attributes.center = CGPoint(x: centerX, y: (self.collectionView!.bounds).midY)
//3
attributes.angle = self.angle + (self.anglePerItem * CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// return an array layout attributes instances for all the views in the given rect
return attributesList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Function in Cell Class:
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * self.bounds.height
}
Add this to controller if you need current cell to add pageControl:
In ViewDidload:
if let layout = collectionView?.collectionViewLayout as? CircularCollectionViewLayout {
layout.delegate = self
}
Extensions:
extension LevelsVC: CircularViewLayoutDelegate {
func getCurrentIndex(value: Int) {
self.pageControl.currentPage = value - 1
}
}

Related

UIKit Dynamics in Collection View always ends in chaos

When scrolling with my "springy flow layout" (meant to replicate the scrolling effect in Messages), the initial scroll works well, but repeated scrolling ends with all of the cells constantly bouncing all over the screen, and off the vertical axis.
I don't understand why the cells are moving off the vertical axis, since there is no movement applied in the horizontal axis. I'm simply applying this flow layout to my collection view which is currently only setup to create a bunch of dummy cells.
How do I prevent movement in the horizontal axis and how do I ensure that the cells always come to a rest eventually
class SpringyColumnLayout: UICollectionViewFlowLayout {
private lazy var dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self)
override func prepare() {
super.prepare()
guard let cv = collectionView else { return }
let availableWidth = cv.bounds.inset(by: cv.layoutMargins).width
let minColumnWidth: CGFloat = 300
let maxNumberOfColumns = Int(availableWidth / minColumnWidth)
let cellWidth = (availableWidth / CGFloat(maxNumberOfColumns))
.rounded(.down)
self.itemSize = CGSize(width: cellWidth, height: 70)
self.sectionInset = UIEdgeInsets(
top: minimumInteritemSpacing,
left: 0,
bottom: 0,
right: 0
)
self.sectionInsetReference = .fromSafeArea
if dynamicAnimator.behaviors.isEmpty {
let contentSize = collectionViewContentSize
let contentBounds = CGRect(origin: .zero, size: contentSize)
guard let items = super.layoutAttributesForElements(in: contentBounds)
else { return }
for item in items {
let spring = UIAttachmentBehavior(
item: item,
attachedToAnchor: item.center
)
spring.length = 0
spring.damping = 0.8
spring.frequency = 1
self.dynamicAnimator.addBehavior(spring)
}
}
}
override func layoutAttributesForElements(
in rect: CGRect
) -> [UICollectionViewLayoutAttributes]? {
return dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes]
}
override func layoutAttributesForItem(
at indexPath: IndexPath
) -> UICollectionViewLayoutAttributes? {
return dynamicAnimator.layoutAttributesForCell(at: indexPath)
}
override func shouldInvalidateLayout(
forBoundsChange newBounds: CGRect
) -> Bool {
let scrollView = self.collectionView!
let scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y
let touchLocation = scrollView.panGestureRecognizer
.location(in: scrollView)
for case let spring as UIAttachmentBehavior in dynamicAnimator.behaviors {
let anchorPoint = spring.anchorPoint
let yDistanceFromTouch = abs(touchLocation.y - anchorPoint.y)
let xDistanceFromTouch = abs(touchLocation.x - anchorPoint.x)
let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500
let item = spring.items.first!
var center = item.center
if scrollDelta < 0 {
center.y += max(scrollDelta, scrollDelta * scrollResistance)
} else {
center.y += min(scrollDelta, scrollDelta * scrollResistance)
}
item.center = center
dynamicAnimator.updateItem(usingCurrentState: item)
}
return false
}
}

How to center each view in the collection with UICollectionViewFlowLayout?

Similar questions are posted here, but the problem relies on my code and I don't know to solve it.
I don't know why, but as I'm scrolling my collectionview, the cells are moving more to the left. See image below:
Here is the code for my UICollectionViewFlowLayout
import UIKit
class PrayerFlowLayout: UICollectionViewFlowLayout {
//let standardItemAlpha: CGFloat = 0.3
let standardItemScale: CGFloat = 0.85
var isSetup = false
override func prepare() {
super.prepare()
if isSetup == false {
setupCollectionView()
isSetup = true
}
}
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var attributesCopy = [UICollectionViewLayoutAttributes]()
for itemAttributes in attributes! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
changeLayoutAttributes(itemAttributesCopy)
attributesCopy.append(itemAttributesCopy)
}
return attributesCopy
}
// indicates the point on where to stop scrolling each prayer
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// get layout attribute to use to make some calculations
let layoutAttributes = self.layoutAttributesForElements(in: collectionView!.bounds)
// get the horizontal center on the collection
let center = collectionView!.frame.size.width / 2
// add the center to the proposed content offset
let proporsedContentOffsetCenterOrigin = proposedContentOffset.x + center
let closest = layoutAttributes!.sorted {
abs($0.center.x - proporsedContentOffsetCenterOrigin) < abs($1.center.x - proporsedContentOffsetCenterOrigin)
}.first ?? UICollectionViewLayoutAttributes()
let targetContentOffset = CGPoint(x: floor(closest.center.x - center), y: proposedContentOffset.y - center)
return targetContentOffset
}
func changeLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) {
let collectionCenter = collectionView!.bounds.width / 2
let offset = collectionView!.contentOffset.x
let normalizedCenter = attributes.center.x - offset
let maxDistance = collectionView!.bounds.size.width + self.minimumLineSpacing
//collectionView!.frame.width + self.minimumLineSpacing // self.itemSize.width + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
//let alpha = ratio * (1 - self.standardItemAlpha) + self.standardItemAlpha
let scale = ratio * (1 - self.standardItemScale) + self.standardItemScale
//attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
}
func setupCollectionView() {
self.collectionView!.decelerationRate = UIScrollView.DecelerationRate.fast
let collectionSize = collectionView!.bounds.size
let yInset = (collectionSize.height - self.itemSize.height) / 3
let xInset = (collectionSize.width - self.itemSize.width) / 2
let topPos = (collectionView!.bounds.height - (collectionView!.bounds.height - 75) )
self.sectionInset = UIEdgeInsets.init(top: topPos, left: xInset, bottom: yInset, right: xInset)
}
}
Any ideas on how I can have all the cells always centered?
You need to use UICollectionViewDelegateFlowLayout method like this
class CardListViewController:UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout{
// UICollectionViewDelegateFlowLayout Delegate method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let leftAndRightPaddings: CGFloat = 20.0
let numberOfItemsPerRow: CGFloat = 1.0
let width = (collectionView.frame.width - leftAndRightPaddings)/numberOfItemsPerRow
return CGSize(width: width, height: collectionView.frame.width)
}
}
From the Storyboard Set Section Insets Like this.
Output :

Rotate collectionView in circle following user direction

I am trying create collectionView with circuler layout and I want the collectionView to rotate in circle as the user swipe his finger on screen round in whatever direction. I found the circle layout for collectionView here is what I have done so far
to rotate this collectionView I have wrote this code
add gesture to collectionView
panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
here is the gestureReader and animation methods
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
print(distance)
if start.x > end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
self.collectionView.transform = CGAffineTransform.identity
self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
}) { (true) in
//
}
}
First the animation is not working properly and second when collectionView gets rotated this is what I get
Question1 : What else do I need to add to make this animation smooth and follow user's finger?
Question2 : I want the collectionViewcells to stay as before animation, how can I achieve this, please help
Thanks in advance
I show you an example here. The decor View S1View is a subclass of UICollectionViewCell with the identifier "background".
The code is not hard to understand but tedious to put together. How to control animator is another story.
class TestCollectionViewLayout: UICollectionViewLayout {
lazy var dataSource : UICollectionViewDataSource? = {
self.collectionView?.dataSource
}()
var layouts : [IndexPath: UICollectionViewLayoutAttributes?] = [:]
var itemNumber : Int {
return dataSource!.collectionView(collectionView!, numberOfItemsInSection: 0)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?{
var itemArray = (0..<itemNumber).map{ self.layoutAttributesForItem(at: IndexPath.init(row: $0, section: 0))!}
itemArray.append(self.layoutAttributesForDecorationView(ofKind:"background"
, at: IndexPath.init(row: 0, section: 0)))
return itemArray
}
override var collectionViewContentSize: CGSize { get{
return self.collectionView?.frame.size ?? CGSize.zero
}
}
lazy var dynamicAnimator = {UIDynamicAnimator(collectionViewLayout: self)}()
private func updateCurrentLayoutAttributesForItem(at indexPath: IndexPath, current: UICollectionViewLayoutAttributes?) -> UICollectionViewLayoutAttributes?{
return current
}
private func initLayoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
let layoutAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let center = (collectionView?.center)!
let angle = (CGFloat(indexPath.row) / CGFloat(itemNumber) * CGFloat.pi * 2)
layoutAttributes.center = CGPoint.init(x: center.x + cos(angle) * CGFloat(radius) , y: center.y + sin(angle) * CGFloat(radius) )
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: 100, height: 100 )
if let decorator = self.decorator {
let itemBehavior =
UIAttachmentBehavior.pinAttachment(with: layoutAttributes, attachedTo: decorator, attachmentAnchor: layoutAttributes.center)
dynamicAnimator.addBehavior(itemBehavior)
layouts[indexPath] = layoutAttributes
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
guard let currentLayout = layouts[indexPath] else {
return initLayoutAttributesForItem(at:indexPath)}
return currentLayout
}
private let radius = 200
private var decorator: UICollectionViewLayoutAttributes?
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes{
guard let decorator = self.decorator else {
let layoutAttributes = UICollectionViewLayoutAttributes.init(forDecorationViewOfKind: elementKind, with: indexPath)
layoutAttributes.center = (self.collectionView?.center)!
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: radius, height: radius)
self.decorator = layoutAttributes
return layoutAttributes
}
return decorator
}
lazy var s: UIDynamicItemBehavior = {
let decorator = self.decorator!
let s = UIDynamicItemBehavior.init(items: [decorator])
s.angularResistance = 1
dynamicAnimator.addBehavior(s)
return s
}()
func rotate(_ speed: CGFloat){
guard let decorator = self.decorator else {return}
s.addAngularVelocity(speed, for: decorator)
}
}
class TestCollectionViewController: UICollectionViewController {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
else if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
if start.x < end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
(collectionView.collectionViewLayout as? TestCollectionViewLayout).map{
$0.rotate(angle / 100)
}
// UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
// self.collectionView.transform = CGAffineTransform.identity
// self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
// }) { (true) in
// //
// }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (Timer) in
// self.rotate()
// }
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = TestCollectionViewLayout()
collectionView.collectionViewLayout.register(UINib.init(nibName: "S1View", bundle: nil) , forDecorationViewOfKind: "background")
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
}
var data: [Int] = [1,2,3,4,5,6,7]
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
}
Maybe this tutorial will help: https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel
Your first problem is that you are rotating the whole collection view. Think of it like you are putting those circles on a piece of paper and then rotating that piece of paper. You don't want to rotate the whole collection view. You might not want to rotate the circles around a point because then the rotation affects the image and text in the circle. You just want to change the circle's position in a circular movement.
If the UICollectionView isn't working, you could ditch it and use regular UIViews and position them in a circular pattern (These functions should help: https://gist.github.com/akhilcb/8d03f1f88f87e996aec24748bdf0ce78). Once you have the views laid out in a circle then you just need to update the angle for each view as the user drags their finger. Store the previous angle on the view and add to it whatever you want when the user drags their finger. Little bit of trial and error and it shouldn't be too bad.
Update
The main reason to use collection views is if you have a lot of items and you need to reuse views like a list. If you don't need to reuse views then using a UICollectionView can be pain to understand, customize and change things. Here is a simple example of using regular views that rotate around a circle using a UIPanGestureRecognizer input.
Example:
import UIKit
class ViewController: UIViewController {
var rotatingViews = [RotatingView]()
let numberOfViews = 8
var circle = Circle(center: CGPoint(x: 200, y: 200), radius: 100)
var prevLocation = CGPoint.zero
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...numberOfViews {
let angleBetweenViews = (2 * Double.pi) / Double(numberOfViews)
let viewOnCircle = RotatingView(circle: circle, angle: CGFloat(Double(i) * angleBetweenViews))
rotatingViews.append(viewOnCircle)
view.addSubview(viewOnCircle)
}
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGesture:)))
view.addGestureRecognizer(panGesture)
}
#objc func didPan(panGesture: UIPanGestureRecognizer){
switch panGesture.state {
case .began:
prevLocation = panGesture.location(in: view)
case .changed, .ended:
let nextLocation = panGesture.location(in: view)
let angle = circle.angleBetween(firstPoint: prevLocation, secondPoint: nextLocation)
rotatingViews.forEach({ $0.updatePosition(angle: angle)})
prevLocation = nextLocation
default: break
}
}
}
struct Circle {
let center: CGPoint
let radius: CGFloat
func pointOnCircle(angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
func angleBetween(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let firstAngle = atan2(firstPoint.y - center.y, firstPoint.x - center.x)
let secondAnlge = atan2(secondPoint.y - center.y, secondPoint.x - center.x)
let angleDiff = (firstAngle - secondAnlge) * -1
return angleDiff
}
}
class RotatingView: UIView {
var currentAngle: CGFloat
let circle: Circle
init(circle: Circle, angle: CGFloat) {
self.currentAngle = angle
self.circle = circle
super.init(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
center = circle.pointOnCircle(angle: currentAngle)
backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePosition(angle: CGFloat) {
currentAngle += angle
center = circle.pointOnCircle(angle: currentAngle)
}
}
Circle is a struct that just holds the center of all the views, how far apart you want them (radius), and helper functions for calculating the angles found in the GitHub link above.
RotatingViews are the views that rotate around the middle.

custom collection view not scaling last image

so I have a custom collectionviewflowlayout that works perfectly but in one of my collection views it is not scaling the image as desired. I have attached some images below, you can see that the last cell is not scaling properly.
I'm not sure why this scaling issue is happening in this cell.
the collectionview is a custom class of collectionviewflowlayout which the snapping of cells to the centre of the screen:
class VerticalCollectionViewFlow: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfHeight = cvBounds.size.height * 0.5
print(proposedContentOffset.y)
let proposedContentOffsetCenterY = proposedContentOffset.y + halfHeight
if let attributesForVisibleCells = self.layoutAttributesForElements(in: cvBounds) {
var candidateAttributes : UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.cell {
continue
}
if (attributes.center.y == 0) || (attributes.center.y > (cv.contentOffset.y + halfHeight) && velocity.y < 0) {
continue
}
// == First time in the loop == //
guard let candAttrs = candidateAttributes else {
candidateAttributes = attributes
continue
}
let a = attributes.center.y - proposedContentOffsetCenterY
let b = candAttrs.center.y - proposedContentOffsetCenterY
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
// Beautification step , I don't know why it works!
if(proposedContentOffset.y == -(cv.contentInset.top)) {
return proposedContentOffset
}
if candidateAttributes != nil {
return CGPoint(x: proposedContentOffset.x, y: floor(candidateAttributes!.center.y - halfHeight))
}
}
}
// fallback
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
and the scaling is handled from another class that is called from viewdidload of the main viewcontroller where the delegates of UIcollectionViewController are set.
import UIKit
public enum SC_ScaledPattern {
case horizontalCenter
case horizontalLeft
case horizontalRight
case verticalCenter
case verticalBottom
case verticalTop
}
open class ScaledVisibleCellsCollectionView {
static let sharedInstance = ScaledVisibleCellsCollectionView()
var maxScale: CGFloat = 1.0
var minScale: CGFloat = 0.5
var maxAlpha: CGFloat = 1.0
var minAlpha: CGFloat = 0.5
var scaledPattern: SC_ScaledPattern = .verticalCenter
}
extension UICollectionView {
/**
Please always set
*/
public func setScaledDesginParam(scaledPattern pattern: SC_ScaledPattern, maxScale: CGFloat, minScale: CGFloat, maxAlpha: CGFloat, minAlpha: CGFloat) {
ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern = pattern
ScaledVisibleCellsCollectionView.sharedInstance.maxScale = maxScale
ScaledVisibleCellsCollectionView.sharedInstance.minScale = minScale
ScaledVisibleCellsCollectionView.sharedInstance.maxAlpha = maxAlpha
ScaledVisibleCellsCollectionView.sharedInstance.minAlpha = minAlpha
}
/**
Please call at any time
*/
public func scaledVisibleCells() {
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .horizontalCenter, .horizontalLeft, .horizontalRight:
scaleCellsForHorizontalScroll(visibleCells)
break
case .verticalCenter, .verticalTop, .verticalBottom:
self.scaleCellsForVerticalScroll(visibleCells)
break
}
}
}
extension UICollectionView {
fileprivate func scaleCellsForHorizontalScroll(_ visibleCells: [UICollectionViewCell]) {
let scalingAreaWidth = bounds.width / 2
let maximumScalingAreaWidth = (bounds.width / 2 - scalingAreaWidth) / 2
for cell in visibleCells {
var distanceFromMainPosition: CGFloat = 0
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .horizontalCenter:
distanceFromMainPosition = horizontalCenter(cell)
break
case .horizontalLeft:
distanceFromMainPosition = abs(cell.frame.midX - contentOffset.x - (cell.bounds.width / 2))
break
case .horizontalRight:
distanceFromMainPosition = abs(bounds.width / 2 - (cell.frame.midX - contentOffset.x) + (cell.bounds.width / 2))
break
default:
return
}
let preferredAry = scaleCells(distanceFromMainPosition, maximumScalingArea: maximumScalingAreaWidth, scalingArea: scalingAreaWidth)
let preferredScale = preferredAry[0]
let preferredAlpha = preferredAry[1]
cell.transform = CGAffineTransform(scaleX: preferredScale, y: preferredScale)
cell.alpha = preferredAlpha
}
}
fileprivate func scaleCellsForVerticalScroll(_ visibleCells: [UICollectionViewCell]) {
let scalingAreaHeight = bounds.height / 2
let maximumScalingAreaHeight = (bounds.height / 2 - scalingAreaHeight) / 2
for cell in visibleCells {
var distanceFromMainPosition: CGFloat = 0
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .verticalCenter:
distanceFromMainPosition = verticalCenter(cell)
break
case .verticalBottom:
distanceFromMainPosition = abs(bounds.height - (cell.frame.midY - contentOffset.y + (cell.bounds.height / 2)))
break
case .verticalTop:
distanceFromMainPosition = abs(cell.frame.midY - contentOffset.y - (cell.bounds.height / 2))
break
default:
return
}
let preferredAry = scaleCells(distanceFromMainPosition, maximumScalingArea: maximumScalingAreaHeight, scalingArea: scalingAreaHeight)
let preferredScale = preferredAry[0]
let preferredAlpha = preferredAry[1]
cell.transform = CGAffineTransform(scaleX: preferredScale, y: preferredScale)
cell.alpha = preferredAlpha
}
}
fileprivate func scaleCells(_ distanceFromMainPosition: CGFloat, maximumScalingArea: CGFloat, scalingArea: CGFloat) -> [CGFloat] {
var preferredScale: CGFloat = 0.0
var preferredAlpha: CGFloat = 0.0
let maxScale = ScaledVisibleCellsCollectionView.sharedInstance.maxScale
let minScale = ScaledVisibleCellsCollectionView.sharedInstance.minScale
let maxAlpha = ScaledVisibleCellsCollectionView.sharedInstance.maxAlpha
let minAlpha = ScaledVisibleCellsCollectionView.sharedInstance.minAlpha
if distanceFromMainPosition < maximumScalingArea {
// cell in maximum-scaling area
preferredScale = maxScale
preferredAlpha = maxAlpha
} else if distanceFromMainPosition < (maximumScalingArea + scalingArea) {
// cell in scaling area
let multiplier = abs((distanceFromMainPosition - maximumScalingArea) / scalingArea)
preferredScale = maxScale - multiplier * (maxScale - minScale)
preferredAlpha = maxAlpha - multiplier * (maxAlpha - minAlpha)
} else {
// cell in minimum-scaling area
preferredScale = minScale
preferredAlpha = minAlpha
}
return [ preferredScale, preferredAlpha ]
}
}
extension UICollectionView {
fileprivate func horizontalCenter(_ cell: UICollectionViewCell)-> CGFloat {
return abs(bounds.width / 2 - (cell.frame.midX - contentOffset.x))
}
fileprivate func verticalCenter(_ cell: UICollectionViewCell)-> CGFloat {
return abs(bounds.height / 2 - (cell.frame.midY - contentOffset.y))
}
}
I have this working on multi collection views, just this one is producing the scaling error.
below is the extension of the collectionview that is handling the sticky nature of the collection/scrollview
extension JourneyViewController: UIScrollViewDelegate {
//------------------------------------------------------------------ make the scrolling a bit sticky
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.collectionViewPart.scaledVisibleCells()
let offsetY = scrollView.contentOffset.x
let contentHeight = scrollView.contentSize.width
if offsetY > contentHeight - scrollView.frame.size.width {
self.collectionViewPart.reloadData()
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startingScrollingOffset = scrollView.contentOffset
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let cellWidth = collectionViewPart.collectionViewLayout.collectionViewContentSize.width
let page: CGFloat
let proposedPage = scrollView.contentOffset.x / cellWidth
// 10% of next cell to change / 90% of previous cell to change
let delta: CGFloat = scrollView.contentOffset.x > startingScrollingOffset.x ? 0.1 : 0.9
if floor(proposedPage + delta) == floor(proposedPage) {
page = floor(proposedPage)
}
else {
page = floor(proposedPage + 1)
}
}
}
hope someone smarter than me can figure this out I've been staring at it for countless hours.
thanks!

Circular UICollectionview, how to keep all cell angles 0°

I build a circular UICollectionview by following This guide, Everything is working as expected but I don't want the items to rotate around their own angle/anchor
The top row is how my circular collectionview is working at the moment, the bottom drawing is how I would like my collectionview :
I am using following layout attribute code:
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
var anchorPoint = CGPoint(x: 0.3, y: 0.5)
var angle: CGFloat = 0 {
didSet {
zIndex = Int(angle*1000000)
transform = CGAffineTransformMakeRotation(angle)
}
}
override func copyWithZone(zone: NSZone) -> AnyObject {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
with the following layout class:
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 60, height: 110)
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItemsInSection(0) > 0 ? -CGFloat(collectionView!.numberOfItemsInSection(0)-1)*anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme*collectionView!.contentOffset.x/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
}
var radius: CGFloat = 400 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return 0.18
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func collectionViewContentSize() -> CGSize {
return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0))*itemSize.width,
height: CGRectGetHeight(collectionView!.bounds))
}
override class func layoutAttributesClass() -> AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
override func prepareLayout() {
super.prepareLayout()
let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds)/2.0)
let anchorPointY = ((itemSize.height/2.0) + radius)/itemSize.height
let theta = atan2(CGRectGetWidth(collectionView!.bounds)/2.0, radius + (itemSize.height/2.0) - (CGRectGetHeight(collectionView!.bounds)/2.0)) //1
//let theta:CGFloat = 1.0
var startIndex = 0
var endIndex = collectionView!.numberOfItemsInSection(0) - 1
if (angle < -theta) {
startIndex = Int(floor((-theta - angle)/anglePerItem))
}
endIndex = min(endIndex, Int(ceil((theta - angle)/anglePerItem)))
if (endIndex < startIndex) {
endIndex = 0
startIndex = 0
}
attributesList = (startIndex...endIndex).map { (i) -> CircularCollectionViewLayoutAttributes in
let attributes = CircularCollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: i, inSection: 0))
attributes.size = self.itemSize
attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
attributes.angle = self.angle + (self.anglePerItem*CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)
-> (UICollectionViewLayoutAttributes!) {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var finalContentOffset = proposedContentOffset
let factor = -angleAtExtreme/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
let proposedAngle = proposedContentOffset.x*factor
let ratio = proposedAngle/anglePerItem
var multiplier: CGFloat
if (velocity.x > 0) {
multiplier = ceil(ratio)
} else if (velocity.x < 0) {
multiplier = floor(ratio)
} else {
multiplier = round(ratio)
}
finalContentOffset.x = multiplier*anglePerItem/factor
return finalContentOffset
}
}
I tried many things but I was not able to change the cell rotation
I've solved this problem by rotateing the view of cell by negative angle:
Code below:
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
super.applyLayoutAttributes(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
viewRoot.transform = CGAffineTransformMakeRotation(-circularlayoutAttributes.angle)
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
}

Resources