Carousel Layout Collection View Issue - ios

Hello, I have followed raywenderlich video for collection view custom
layout. UICollectionViewFlowLayout is subclassed as below
import UIKit
class CharacterFlowLayout: UICollectionViewFlowLayout {
let standardItemAlpha: CGFloat = 0.5
let standardItemScale: CGFloat = 0.5
var count = 0
override func prepare() {
super.prepare()
}
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
}
**override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool** {
return true
}
**func changeLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes)** {
let collectionCenter = collectionView!.frame.size.height/2
let offset = collectionView!.contentOffset.y // bounds
let normalizedCenter = attributes.center.y - offset
let maxDistance = self.itemSize.height + 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)
} }
I don't understand the logic behind the calculation? Could you please explain why we are calculating distance and ratio?

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 :

How to set drop shadow around a section for UITableView in iOS Swift 4?

I want to set shadow around a every section(group of cells) in iOS Swift.
Like this:
Easy to achieve this with UICollectionView ,
by subclass UICollectionViewFlowLayout with DecorationView
turn UITableViewCell into UICollectionViewCell ,
use decoration view to achieve the drop shadow of section part
class DecorationFlow: UICollectionViewFlowLayout {
private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
register(HistoDecorationV.self, forDecorationViewOfKind: HistoDecorationV.id)
let originX: CGFloat = 15
let widH: CGFloat = UI.std.width - originX * 2
guard let collect = collectionView else{
return
}
let sections = collect.numberOfSections
var originY: CGFloat = 0
for sect in 0..<sections{
let sectionFirst = IndexPath(item: 0, section: sect)
let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: HistoDecorationV.id, with: sectionFirst)
let itemCount = collect.numberOfItems(inSection: sect)
originY = originY + 80
let h: CGFloat = 50 * CGFloat(itemCount)
attributes.frame = CGRect(x: originX, y: originY, width: widH, height: h)
originY = originY + h + 15
cache[sectionFirst] = attributes
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
private func prepareCache() {
cache.removeAll(keepingCapacity: true)
cache = [IndexPath: UICollectionViewLayoutAttributes]()
}
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var array = super.layoutAttributesForElements(in: rect)
guard let collection = collectionView, collection.numberOfSections > 0 else{
return array
}
var z = 0
array?.forEach({
$0.zIndex = z + 100
z += 1
})
z = 0
for (_, attributes) in cache {
if attributes.frame.intersects(rect){
attributes.zIndex = z + 10
array?.append(attributes)
}
z += 1
}
return array
}
}
more code in github

Animate Collection View Bounds Change with Custom Layout Animation

I have a collection view and I want it to have 2 states: collapsed and expanded.
Here are the two states:
The collection view has a red background, the cells have a purple background, and the containing view of the collection view has a gray background.
I am using a custom subclass of UICollectionViewFlowLayout that handles paging, scaling, and opacity of the cells. The layout is supposed to assure that one of the cells is always centered.
What I'm trying to achieve
A smooth animation between the two states where the cells simply grow/shrink with the collection view.
What I'm experiencing
Since the width of the cell changes between the two states, I'm seeing not only the default fade animation between the cells, but also the cell that was centered in one state is no longer centered.
What I tried
I tried following the UPCarouselLayout for the initial layout. I tried following this objc.io post for animation ideas and specifically the section on device rotations. I wasn't able to use their approach because the frame returned by layoutAttributesForElement(at indexPath: IndexPath) was incorrect.
The layout code
protocol TrayCarouselFlowLayoutDelegate: class {
func sizeForItemInCarousel(_ collectionView: UICollectionView, _ layout: TrayCarouselFlowLayout) -> CGSize
}
class TrayCarouselFlowLayout: UICollectionViewFlowLayout {
fileprivate var collectionViewSize: CGSize = .zero
fileprivate var peekItemScale: CGFloat = 0.95
fileprivate var peekItemAlpha: CGFloat = 0.65
fileprivate var peekItemShift: CGFloat = 0.0
var spacing = 16
weak var delegate: TrayCarouselFlowLayoutDelegate!
fileprivate var indexPathsToAnimate: [IndexPath] = []
override func prepare() {
super.prepare()
guard let size = collectionView?.bounds.size, collectionViewSize != size else { return }
setUpCollectionView()
updateLayout()
collectionViewSize = size
}
fileprivate func setUpCollectionView() {
guard let collectionView = collectionView else { return }
if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
}
fileprivate func updateLayout() {
guard let collectionView = collectionView else { return }
itemSize = delegate.sizeForItemInCarousel(collectionView, self)
let yInset = (collectionView.bounds.height - itemSize.height) / 2
let xInset = (collectionView.bounds.width - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: yInset, left: xInset, bottom: yInset, right: xInset)
let side = itemSize.width
let scaledItemOffset = (side - side * peekItemScale) / 2
minimumLineSpacing = spacing - scaledItemOffset
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
let oldBounds = collectionView?.bounds ?? .zero
if oldBounds != newBounds {
return true
}
return false
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map { self.transformLayoutAttributes($0) }
}
fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = collectionView else { return attributes }
let collectionCenter = collectionView.frame.size.width / 2
let offset = collectionView.contentOffset.x
let normalizedCenter = attributes.center.x - offset
let maxDistance = itemSize.width + minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance) / maxDistance
let alpha = ratio * (1 - peekItemAlpha) + peekItemAlpha
let scale = ratio * (1 - peekItemScale) + peekItemScale
let shift = (1 - ratio) * peekItemShift
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)
attributes.center.y += shift
return attributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView, !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let midSide: CGFloat = collectionViewSize.width / 2
let proposedContentOffsetCenterOrigin = proposedContentOffset.x + midSide
let closest = layoutAttributes
.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }
.first ?? UICollectionViewLayoutAttributes()
let targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
return targetContentOffset
}
}
What I need help with
My goal is to simply grow the collection view and the cells along with it. Since I'm changing the section insets and the size of the cell in my code, the most challenging part is keeping the cell centered during the growth/shrinking. Any help would be greatly appreciated!
EDIT: If I don't change the width of the cell when expanding/collapsing, this behaves ALMOST how I want it. There is still a fade animation but that would be acceptable if it was the only part off the mark. For some reason, redefining the width while the layout cells are growing is what is causing the issue seen in the GIF.
Here is a GIF where the width is static:

Circular wheel animation implementation in 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
}
}

Resources