How can I add the arrow of the maximum and minimum value in iOS-charts? - ios-charts

I want to show the arrow of the maximum and minimum value just like the picture below.
Does anyone have ideas about how to achieve it using iOS-Charts? Thanks a lot.

You need to write your own custom renderer for CandleStickChartView.
Fortunately, in your case you need overwrite only one method.
So, inherit class from CandleStickChartRenderer and override method drawValues(context: CGContext). You can just copy/paste most of the code from parent's method and make only necessary changes.
class MyCandleStickChartRenderer: CandleStickChartRenderer {
internal var _xBounds = XBounds()
var minValue: Double
var maxValue: Double
init (view: CandleStickChartView, minValue: Double, maxValue: Double) {
self.minValue = minValue
self.maxValue = maxValue
super.init(dataProvider: view, animator: view.chartAnimator, viewPortHandler: view.viewPortHandler)
}
override func drawValues(context: CGContext)
{
// ... I remove some code that was not changed ...
for j in stride(from: _xBounds.min, through: _xBounds.range + _xBounds.min, by: 1)
{
guard let e = dataSet.entryForIndex(j) as? CandleChartDataEntry else { break }
// need to show only min and max values
guard e.high == maxValue || e.low == minValue else { continue }
pt.x = CGFloat(e.x)
if e.high == maxValue {
pt.y = CGFloat(e.high * phaseY)
} else if e.low == minValue {
pt.y = CGFloat(e.low * phaseY)
}
pt = pt.applying(valueToPixelMatrix)
// ... I remove some code that was not changed ...
if dataSet.isDrawValuesEnabled
{
var textValue: String?
var align: NSTextAlignment = .center
// customize position for min/max value
if e.high == maxValue {
pt.y -= yOffset
textValue = "← " + String(maxValue)
align = .left
} else if e.low == minValue {
pt.y += yOffset / 5
textValue = String(minValue) + " →"
align = .right
}
if let textValue = textValue {
ChartUtils.drawText(
context: context,
text: textValue,
point: CGPoint(
x: pt.x,
y: pt.y ),
align: align,
attributes: [NSAttributedStringKey.font: valueFont, NSAttributedStringKey.foregroundColor: dataSet.valueTextColorAt(j)])
}
}
}
}
}
}
Also you need to create instance of your renderer and set it as property in your chart view.
myCandleStickChartView.renderer = MyCandleStickChartRenderer(view: candleStickChartView, minValue: 400, maxValue: 1450)
And in the end ...

Related

How to create a function that will add blocks to a grid?

I have this code that creates a 15x12 grid of blocks, but I am trying to access the index of each block and be able to remove or add blocks at that given index. I think I need to use a 2d array, but I am not sure how to add the blocks to an array that will make it easy for me to get each block at their index. Please comment any tips or advice you have, thanks!
for i in 0...12{
for j in 0...16{
let block = SKSpriteNode(texture: blockImage, size: blockSize)
block.position.x = block.frame.width/2 + CGFloat((64*j))
block.position.y = frame.height - block.frame.height/2 - CGFloat((64*i))
block.zPosition = 1
addChild(block)
}
}
Let's follow the idea you proposed.
Step 1
We can create a Dictionary were we map the index of a node (in the key) with the node (in the value).
struct Index: Hashable {
let i: Int
let j: Int
}
private var grid:[Index: SKNode] = [:]
Step 2
Now when you are adding the nodes to the parent, you just need to save the Index-Node relationship into the dictionary.
func addSprites() {
let blockImage = SKTexture(imageNamed: "TODO")
let blockSize = blockImage.size()
for i in 0...12{
for j in 0...16{
let block = SKSpriteNode(texture: blockImage, size: blockSize)
assert(grid[Index(i: i, j: j)] == nil)
grid[Index(i: i, j: j)] = block // <------------
block.position.x = block.frame.width/2 + CGFloat((64*j))
block.position.y = frame.height - block.frame.height/2 - CGFloat((64*i))
block.zPosition = 1
addChild(block)
}
}
}
Step 3
And finally you can easily remove a node for a given index
func removeSprite(at index: Index) {
guard let node = grid[index] else {
debugPrint("No node found at index: \(index)")
return
}
node.removeFromParent()
grid[index] = nil
}
Full code
class GameScene: SKScene {
struct Index: Hashable {
let i: Int
let j: Int
}
private var grid:[Index: SKNode] = [:]
func addSprites() {
let blockImage = SKTexture(imageNamed: "TODO")
let blockSize = blockImage.size()
for i in 0...4{
for j in 0...4{
let block = SKSpriteNode(texture: blockImage, size: blockSize)
assert(grid[Index(i: i, j: j)] == nil)
grid[Index(i: i, j: j)] = block // <---
block.position.x = block.frame.width/2 + CGFloat((64*j))
block.position.y = frame.height - block.frame.height/2 - CGFloat((64*i))
block.zPosition = 1
addChild(block)
}
}
}
func removeSprite(at index: Index) {
guard let node = grid[index] else {
debugPrint("No node found at index: \(index)")
return
}
node.removeFromParent()
grid[index] = nil
}
}
Quick Playground Test

iOS Charts - Display highest and lowest value for CandleStick charts

I'm trying to create a candlestick chart using Charts
As you guys can notice from my screenshot, the chart only shows the highest and lowest values instead of displaying the values for all the candles. Is there any way I can implement that with the Charts framework?
Thanks in advance.
If you want to display only highest and lowest values, you need to implement you own renderer inherited from CandleStickChartRenderer. In fact you just need to override one function drawValues(context: CGContext).
I have made some example which contain a hundred lines of code, but in fact my custom code contains about thirty lines.
class MyCandleStickChartRenderer: CandleStickChartRenderer {
private var _xBounds = XBounds() // Reusable XBounds object
private var minValue: Double
private var maxValue: Double
// New constructor
init (view: CandleStickChartView, minValue: Double, maxValue: Double) {
self.minValue = minValue
self.maxValue = maxValue
super.init(dataProvider: view, animator: view.chartAnimator, viewPortHandler: view.viewPortHandler)
}
// Override draw function
override func drawValues(context: CGContext)
{
guard
let dataProvider = dataProvider,
let candleData = dataProvider.candleData
else { return }
guard isDrawingValuesAllowed(dataProvider: dataProvider) else { return }
var dataSets = candleData.dataSets
let phaseY = animator.phaseY
var pt = CGPoint()
for i in 0 ..< dataSets.count
{
guard let dataSet = dataSets[i] as? IBarLineScatterCandleBubbleChartDataSet
else { continue }
let valueFont = dataSet.valueFont
let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)
let valueToPixelMatrix = trans.valueToPixelMatrix
_xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator)
let lineHeight = valueFont.lineHeight
let yOffset: CGFloat = lineHeight + 5.0
for j in stride(from: _xBounds.min, through: _xBounds.range + _xBounds.min, by: 1)
{
guard let e = dataSet.entryForIndex(j) as? CandleChartDataEntry else { break }
guard e.high == maxValue || e.low == minValue else { continue }
pt.x = CGFloat(e.x)
if e.high == maxValue {
pt.y = CGFloat(e.high * phaseY)
} else if e.low == minValue {
pt.y = CGFloat(e.low * phaseY)
}
pt = pt.applying(valueToPixelMatrix)
if (!viewPortHandler.isInBoundsRight(pt.x))
{
break
}
if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y))
{
continue
}
if dataSet.isDrawValuesEnabled
{
// In this part we draw min and max values
var textValue: String?
var align: NSTextAlignment = .center
if e.high == maxValue {
pt.y -= yOffset
textValue = "← " + String(maxValue)
align = .left
} else if e.low == minValue {
pt.y += yOffset / 5
textValue = String(minValue) + " →"
align = .right
}
if let textValue = textValue {
ChartUtils.drawText(
context: context,
text: textValue,
point: CGPoint(
x: pt.x,
y: pt.y ),
align: align,
attributes: [NSAttributedStringKey.font: valueFont, NSAttributedStringKey.foregroundColor: dataSet.valueTextColorAt(j)])
}
}
}
}
}
}
Do not forget use you custom renderer for you chart. ;)
myCandleStickChartView.renderer = MyCandleStickChartRenderer(view: myCandleStickChartView, minValue: 400, maxValue: 1450)

Procedural Level Generation With Cellular Automaton In Swift

Is there a easy way to create a procedural level with a cellular automaton in swift/SpriteKit(library?)? I want to create a 'cave' with 11 fields in the height and 22 width. These should be randomly created and every field without a wall should be reached.
I just found a documentation using Objective-C, which I am not familiar with. I spend quite some time trying to understand the code and follow the example without success.
PS: If there is an easier way I appreciate some algorithms
I made a Playground where you can experiment
//: Playground - noun: a place where people can play
import UIKit
import SpriteKit
import XCPlayground
class Cave {
var cellmap:[[Bool]]
let chanceToStartAlive = 35
let deathLimit = 3
let birthLimit = 4
var xCell = 40 // number of cell in x axes
var yCell = 20 // number of cell in y axes
var wCell = 20 // cell width
var hCell = 20 // cell height
init(){
cellmap = Array(count:yCell, repeatedValue:
Array(count:xCell, repeatedValue:false))
cellmap = self.initialiseMap(xCell, yIndex:yCell)
}
func initialiseMap(xIndex:Int, yIndex:Int) -> [[Bool]]{
var map:[[Bool]] = Array(count:yIndex, repeatedValue:
Array(count:xIndex, repeatedValue:false))
for y in 0...(yIndex - 1) {
for x in 0...(xIndex - 1) {
let diceRoll = Int(arc4random_uniform(100))
if diceRoll < chanceToStartAlive {
map[y][x] = true
} else {
map[y][x] = false
}
}
}
return map
}
func addSprite(scene:SKScene){
for (indexY, row) in cellmap.enumerate(){
for (indexX, isWall) in row.enumerate(){
if isWall {
let wall = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: wCell, height: hCell))
wall.position = CGPoint(x: (indexX * wCell) + (wCell / 2) , y: (indexY * hCell) + (hCell / 2) )
scene.addChild(wall)
}
}
}
}
func countAliveNeighbours(x:Int, y:Int) -> Int{
var count = 0
var neighbour_x = 0
var neighbour_y = 0
for i in -1...1 {
for j in -1...1 {
neighbour_x = x + j
neighbour_y = y + i
if(i == 0 && j == 0){
} else if(neighbour_x < 0 || neighbour_y < 0 || neighbour_y >= cellmap.count || neighbour_x >= cellmap[0].count){
count = count + 1
} else if(cellmap[neighbour_y][neighbour_x]){
count = count + 1
}
}
}
return count
}
func applyRules(){
var newMap:[[Bool]] = Array(count:yCell, repeatedValue:
Array(count:xCell, repeatedValue:false))
for y in 0...(cellmap.count - 1) {
for x in 0...(cellmap[0].count - 1) {
let nbs = countAliveNeighbours( x, y: y);
if(cellmap[y][x]){
if(nbs < deathLimit){
newMap[y][x] = false;
}
else{
newMap[y][x] = true;
}
} else{
if(nbs > birthLimit){
newMap[y][x] = true;
}
else{
newMap[y][x] = false;
}
}
}
}
cellmap = newMap
}
}
let view:SKView = SKView(frame: CGRectMake(0, 0, 1024, 768))
XCPShowView("Live View", view: view)
let scene:SKScene = SKScene(size: CGSizeMake(1024, 768))
scene.scaleMode = SKSceneScaleMode.AspectFit
let aCave = Cave()
aCave.applyRules()
aCave.applyRules()
aCave.addSprite(scene)
view.presentScene(scene)
Updated the playground code for Xcode 8 and Swift 3. I swapped the X and Y cell count since you will likely see the view in a "portrait" orientation.
Remember to open the Assistant Editor to view the results. It also takes a little while to execute, so give it a couple of minutes to run the algorithm.
//: Playground - noun: a place where people can play
import UIKit
import SpriteKit
import XCPlayground
import PlaygroundSupport
class Cave {
var cellmap:[[Bool]]
let chanceToStartAlive = 35
let deathLimit = 3
let birthLimit = 4
var xCell = 20 // number of cell in x axes
var yCell = 40 // number of cell in y axes
var wCell = 20 // cell width
var hCell = 20 // cell height
init(){
cellmap = Array(repeating:
Array(repeating:false, count:xCell), count:yCell)
cellmap = self.initialiseMap(xIndex: xCell, yIndex:yCell)
}
func initialiseMap(xIndex:Int, yIndex:Int) -> [[Bool]]{
var map:[[Bool]] = Array(repeating:
Array(repeating:false, count:xIndex), count:yIndex)
for y in 0...(yIndex - 1) {
for x in 0...(xIndex - 1) {
let diceRoll = Int(arc4random_uniform(100))
if diceRoll < chanceToStartAlive {
map[y][x] = true
} else {
map[y][x] = false
}
}
}
return map
}
func addSprite(scene:SKScene){
for (indexY, row) in cellmap.enumerated(){
for (indexX, isWall) in row.enumerated(){
if isWall {
let wall = SKSpriteNode(color: UIColor.red, size: CGSize(width: wCell, height: hCell))
wall.position = CGPoint(x: (indexX * wCell) + (wCell / 2) , y: (indexY * hCell) + (hCell / 2) )
scene.addChild(wall)
}
}
}
}
func countAliveNeighbours(x:Int, y:Int) -> Int{
var count = 0
var neighbour_x = 0
var neighbour_y = 0
for i in -1...1 {
for j in -1...1 {
neighbour_x = x + j
neighbour_y = y + i
if(i == 0 && j == 0){
} else if(neighbour_x < 0 || neighbour_y < 0 || neighbour_y >= cellmap.count || neighbour_x >= cellmap[0].count){
count = count + 1
} else if(cellmap[neighbour_y][neighbour_x]){
count = count + 1
}
}
}
return count
}
func applyRules(){
var newMap:[[Bool]] = Array(repeating:
Array(repeating:false, count:xCell), count:yCell)
for y in 0...(cellmap.count - 1) {
for x in 0...(cellmap[0].count - 1) {
let nbs = countAliveNeighbours( x: x, y: y);
if(cellmap[y][x]){
if(nbs < deathLimit){
newMap[y][x] = false;
}
else{
newMap[y][x] = true;
}
} else{
if(nbs > birthLimit){
newMap[y][x] = true;
}
else{
newMap[y][x] = false;
}
}
}
}
cellmap = newMap
}
}
let view:SKView = SKView(frame: CGRect(x: 0, y: 0, width: 768, height: 1024))
let scene:SKScene = SKScene(size: CGSize(width: 768, height: 1024))
scene.scaleMode = SKSceneScaleMode.aspectFit
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = view
let aCave = Cave()
aCave.applyRules()
aCave.applyRules()
aCave.addSprite(scene: scene)
view.presentScene(scene)

Moving multiple sprite nodes at once on swift

Can I make an array of SK nodes of which one is selected randomly and brought from the top to bottom of the screen. For example say I have 25 or so different platforms that will be falling out of the sky on a portrait iPhone. I need it to randomly select one of the platforms from the array to start and then after a certain amount of time/ or pixel space randomly select another to continue the same action until reaching the bottom etc. Im new to swift but have a pretty decent understanding of it. I haven't been able to find out how to create an array of SKsprite nodes yet either. Could someone help with this?
So far the only way I've been able to get any sort of effect similar to what I've wanted is by placing each of the nodes off the screen and adding them to a dictionary and making them move like this
class ObstacleStatus {
var isMoving = false
var timeGapForNextRun = Int(0)
var currentInterval = Int(0)
init(isMoving: Bool, timeGapForNextRun: Int, currentInterval: Int) {
self.isMoving = isMoving
self.timeGapForNextRun = timeGapForNextRun
self.currentInterval = currentInterval
}
func shouldRunBlock() -> Bool {
return self.currentInterval > self.timeGapForNextRun
}
and
func moveBlocks(){
for(blocks, ObstacleStatus) in self.blockStatuses {
var thisBlock = self.childNodeWithName(blocks)
var thisBlock2 = self.childNodeWithName(blocks)
if ObstacleStatus.shouldRunBlock() {
ObstacleStatus.timeGapForNextRun = randomNum()
ObstacleStatus.currentInterval = 0
ObstacleStatus.isMoving = true
}
if ObstacleStatus.isMoving {
if thisBlock?.position.y > blockMaxY{
thisBlock?.position.y -= CGFloat(self.fallSpeed)
}else{
thisBlock?.position.y = self.origBlockPosistionY
ObstacleStatus.isMoving = false
}
}else{
ObstacleStatus.currentInterval++
}
}
}
using this for the random function
func randomNum() -> Int{
return randomInt(50, max: 300)
}
func randomInt(min: Int, max:Int) -> Int {
return min + Int(arc4random_uniform(UInt32(max - min + 1)))
}
All this has been doing for me is moving the pieces down at random timed intervals often overlapping them, But increasing the min or max of the random numbers doesn't really have an affect on the actual timing of the gaps. I need to be able to specify a distance or time gap.
One of many possible solutions is to create a falling action sequence which calls itself recursively until no more platform nodes are left. You can control the mean "gap time" and the range of its random variation. Here is a working example (assuming the iOS SpriteKit game template):
import SpriteKit
extension Double {
var cg: CGFloat { return CGFloat(self) }
}
extension Int {
var cg: CGFloat { return CGFloat(self) }
}
func randomInt(range: Range<Int>) -> Int {
return range.startIndex + Int(arc4random_uniform(UInt32(range.endIndex - range.startIndex)))
}
extension Array {
func randomElement() -> Element? {
switch self.count {
case 0: return nil
default: return self[randomInt(0..<self.count)]
}
}
func apply<Ignore>(f: (T) -> (Ignore)) {
for e in self { f(e) }
}
}
class GameScene: SKScene {
var screenWidth: CGFloat { return UIScreen.mainScreen().bounds.size.width }
var screenHeight: CGFloat { return UIScreen.mainScreen().bounds.size.height }
let PlatformName = "Platform"
let FallenPlatformName = "FallenPlatform"
func createRectangularNode(#x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> SKShapeNode {
let rect = CGRect(x: x, y: y, width: width, height: height)
let path = UIBezierPath(rect: rect)
let node = SKShapeNode(path: path.CGPath)
return node
}
func createPlatformNodes(numNodes: Int, atHeight: CGFloat) -> [SKShapeNode] {
var padding = 20.cg
let width = (screenWidth - padding) / numNodes.cg - padding
padding = (screenWidth - width * numNodes.cg) / (numNodes.cg + 1)
let height = width / 4
var nodes = [SKShapeNode]()
for x in stride(from: padding, to: numNodes.cg * (width + padding), by: width + padding) {
let node = createRectangularNode(x: x, y: atHeight, width: width, height: height)
node.fillColor = SKColor.blackColor()
node.name = PlatformName
nodes.append(node)
}
return nodes
}
func createFallingAction(#by: CGFloat, duration: NSTimeInterval, timeGap: NSTimeInterval, range: NSTimeInterval = 0) -> SKAction {
let gap = SKAction.waitForDuration(timeGap, withRange: range)
// let fall = SKAction.moveToY(toHeight, duration: duration) // moveToY appears to have a bug: behaves as moveBy
let fall = SKAction.moveByX(0, y: -by, duration: duration)
let next = SKAction.customActionWithDuration(0) { [unowned self]
node, time in
node.name = self.FallenPlatformName
self.fallNextNode()
}
return SKAction.sequence([gap, fall, next])
}
func fallNextNode() {
if let nextNode = self[PlatformName].randomElement() as? SKShapeNode {
let falling = createFallingAction(by: screenHeight * 0.7, duration: 1, timeGap: 2.5, range: 2) // mean time gap and random range
nextNode.runAction(falling)
} else {
self.children.apply { ($0 as? SKShapeNode)?.fillColor = SKColor.redColor() }
}
}
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.whiteColor()
for platform in createPlatformNodes(7, atHeight: screenHeight * 0.8) {
self.addChild(platform)
}
fallNextNode()
}
}

targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout

I've got a very simple collectionView in my app (just a single row of square thumbnail images).
I'd like to intercept the scrolling so that the offset always leaves a full image at the left side. At the moment it scrolls to wherever and will leave cut off images.
Anyway, I know I need to use the function
- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity
to do this but I'm just using a standard UICollectionViewFlowLayout. I'm not subclassing it.
Is there any way of intercepting this without subclassing UICollectionViewFlowLayout?
Thanks
OK, answer is no, there is no way to do this without subclassing UICollectionViewFlowLayout.
However, subclassing it is incredibly easy for anyone who is reading this in the future.
First I set up the subclass call MyCollectionViewFlowLayout and then in interface builder I changed the collection view layout to Custom and selected my flow layout subclass.
Because you're doing it this way you can't specify items sizes, etc... in IB so in MyCollectionViewFlowLayout.m I have this...
- (void)awakeFromNib
{
self.itemSize = CGSizeMake(75.0, 75.0);
self.minimumInteritemSpacing = 10.0;
self.minimumLineSpacing = 10.0;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
}
This sets up all the sizes for me and the scroll direction.
Then ...
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalOffset = proposedContentOffset.x + 5;
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
for (UICollectionViewLayoutAttributes *layoutAttributes in array) {
CGFloat itemOffset = layoutAttributes.frame.origin.x;
if (ABS(itemOffset - horizontalOffset) < ABS(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
This ensures that the scrolling ends with a margin of 5.0 on the left hand edge.
That's all I needed to do. I didn't need to set the flow layout in code at all.
Dan's solution is flawed. It does not handle user flicking well. The cases when user flicks fast and scroll did not move so much, have animation glitches.
My proposed alternative implementation has the same pagination as proposed before, but handles user flicking between pages.
#pragma mark - Pagination
- (CGFloat)pageWidth {
return self.itemSize.width + self.minimumLineSpacing;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat rawPageValue = self.collectionView.contentOffset.x / self.pageWidth;
CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);
BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
BOOL flicked = fabs(velocity.x) > [self flickVelocity];
if (pannedLessThanAPage && flicked) {
proposedContentOffset.x = nextPage * self.pageWidth;
} else {
proposedContentOffset.x = round(rawPageValue) * self.pageWidth;
}
return proposedContentOffset;
}
- (CGFloat)flickVelocity {
return 0.3;
}
Swift version of the accepted answer.
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
Valid for Swift 5.
Here's my implementation in Swift 5 for vertical cell-based paging:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// Page height used for estimating and calculating paging.
let pageHeight = self.itemSize.height + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = collectionView.contentOffset.y/pageHeight
// Determine the current page based on velocity.
let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))
// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.3
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}
Some notes:
Doesn't glitch
SET PAGING TO FALSE! (otherwise this won't work)
Allows you to set your own flickvelocity easily.
If something is still not working after trying this, check if your itemSize actually matches the size of the item as that's often a problem, especially when using collectionView(_:layout:sizeForItemAt:), use a custom variable with the itemSize instead.
This works best when you set self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.
Here's a horizontal version (haven't tested it thoroughly so please forgive any mistakes):
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// Page width used for estimating and calculating paging.
let pageWidth = self.itemSize.width + self.minimumInteritemSpacing
// Make an estimation of the current page position.
let approximatePage = collectionView.contentOffset.x/pageWidth
// Determine the current page based on velocity.
let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage))
// Create custom flickVelocity.
let flickVelocity = velocity.x * 0.3
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// Calculate newHorizontalOffset.
let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left
return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}
This code is based on the code I use in my personal project, you can check it out here by downloading it and running the Example target.
For anyone looking for a solution that...
DOES NOT GLITCH when the user performs a short fast scroll (i.e. it considers positive and negative scroll velocities)
takes the collectionView.contentInset (and safeArea on iPhone X) into consideration
only considers thoes cells visible at the point of scrolling (for peformance)
uses well named variables and comments
is Swift 4
then please see below...
public class CarouselCollectionViewLayout: UICollectionViewFlowLayout {
override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
// Identify the layoutAttributes of cells in the vicinity of where the scroll view will come to rest
let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
let visibleCellsLayoutAttributes = layoutAttributesForElements(in: targetRect)
// Translate those cell layoutAttributes into potential (candidate) scrollView offsets
let candidateOffsets: [CGFloat]? = visibleCellsLayoutAttributes?.map({ cellLayoutAttributes in
if #available(iOS 11.0, *) {
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left - sectionInset.left
} else {
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - sectionInset.left
}
})
// Now we need to work out which one of the candidate offsets is the best one
let bestCandidateOffset: CGFloat
if velocity.x > 0 {
// If the scroll velocity was POSITIVE, then only consider cells/offsets to the RIGHT of the proposedContentOffset.x
// Of the cells/offsets to the right, the NEAREST is the `bestCandidate`
// If there is no nearestCandidateOffsetToLeft then we default to the RIGHT-MOST (last) of ALL the candidate cells/offsets
// (this handles the scenario where the user has scrolled beyond the last cell)
let candidateOffsetsToRight = candidateOffsets?.toRight(ofProposedOffset: proposedContentOffset.x)
let nearestCandidateOffsetToRight = candidateOffsetsToRight?.nearest(toProposedOffset: proposedContentOffset.x)
bestCandidateOffset = nearestCandidateOffsetToRight ?? candidateOffsets?.last ?? proposedContentOffset.x
}
else if velocity.x < 0 {
// If the scroll velocity was NEGATIVE, then only consider cells/offsets to the LEFT of the proposedContentOffset.x
// Of the cells/offsets to the left, the NEAREST is the `bestCandidate`
// If there is no nearestCandidateOffsetToLeft then we default to the LEFT-MOST (first) of ALL the candidate cells/offsets
// (this handles the scenario where the user has scrolled beyond the first cell)
let candidateOffsetsToLeft = candidateOffsets?.toLeft(ofProposedOffset: proposedContentOffset.x)
let nearestCandidateOffsetToLeft = candidateOffsetsToLeft?.nearest(toProposedOffset: proposedContentOffset.x)
bestCandidateOffset = nearestCandidateOffsetToLeft ?? candidateOffsets?.first ?? proposedContentOffset.x
}
else {
// If the scroll velocity was ZERO we consider all `candidate` cells (regarless of whether they are to the left OR right of the proposedContentOffset.x)
// The cell/offset that is the NEAREST is the `bestCandidate`
let nearestCandidateOffset = candidateOffsets?.nearest(toProposedOffset: proposedContentOffset.x)
bestCandidateOffset = nearestCandidateOffset ?? proposedContentOffset.x
}
return CGPoint(x: bestCandidateOffset, y: proposedContentOffset.y)
}
}
fileprivate extension Sequence where Iterator.Element == CGFloat {
func toLeft(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {
return filter() { candidateOffset in
return candidateOffset < proposedOffset
}
}
func toRight(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {
return filter() { candidateOffset in
return candidateOffset > proposedOffset
}
}
func nearest(toProposedOffset proposedOffset: CGFloat) -> CGFloat? {
guard let firstCandidateOffset = first(where: { _ in true }) else {
// If there are no elements in the Sequence, return nil
return nil
}
return reduce(firstCandidateOffset) { (bestCandidateOffset: CGFloat, candidateOffset: CGFloat) -> CGFloat in
let candidateOffsetDistanceFromProposed = fabs(candidateOffset - proposedOffset)
let bestCandidateOffsetDistancFromProposed = fabs(bestCandidateOffset - proposedOffset)
if candidateOffsetDistanceFromProposed < bestCandidateOffsetDistancFromProposed {
return candidateOffset
}
return bestCandidateOffset
}
}
}
While this answer has been a great help to me, there is a noticeable flicker when you swipe fast on a small distance. It's much easier to reproduce it on the device.
I found that this always happens when collectionView.contentOffset.x - proposedContentOffset.x and velocity.x have different sings.
My solution was to ensure that proposedContentOffset is more than contentOffset.x if velocity is positive, and less if it is negative. It's in C# but should be fairly simple to translate to Objective C:
public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
/* Determine closest edge */
float offSetAdjustment = float.MaxValue;
float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));
RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
var array = base.LayoutAttributesForElementsInRect (targetRect);
foreach (var layoutAttributes in array) {
float itemHorizontalCenter = layoutAttributes.Center.X;
if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
offSetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
float nextOffset = proposedContentOffset.X + offSetAdjustment;
/*
* ... unless we end up having positive speed
* while moving left or negative speed while moving right.
* This will cause flicker so we resort to finding next page
* in the direction of velocity and use it.
*/
do {
proposedContentOffset.X = nextOffset;
float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
float velX = scrollingVelocity.X;
// If their signs are same, or if either is zero, go ahead
if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
break;
// Otherwise, look for the closest page in the right direction
nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
} while (IsValidOffset (nextOffset));
return proposedContentOffset;
}
bool IsValidOffset (float offset)
{
return (offset >= MinContentOffset && offset <= MaxContentOffset);
}
This code is using MinContentOffset, MaxContentOffset and SnapStep which should be trivial for you to define. In my case they turned out to be
float MinContentOffset {
get { return -CollectionView.ContentInset.Left; }
}
float MaxContentOffset {
get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}
float SnapStep {
get { return ItemSize.Width + MinimumLineSpacing; }
}
After long testing I found solution to snap to center with custom cell width (each cell has diff. width) which fixes the flickering. Feel free to improve the script.
- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint) proposedContentOffset withScrollingVelocity: (CGPoint)velocity
{
CGFloat offSetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));
//setting fastPaging property to NO allows to stop at page on screen (I have pages lees, than self.collectionView.bounds.size.width)
CGRect targetRect = CGRectMake(self.fastPaging ? proposedContentOffset.x : self.collectionView.contentOffset.x,
0.0,
self.collectionView.bounds.size.width,
self.collectionView.bounds.size.height);
NSArray *attributes = [self layoutAttributesForElementsInRect:targetRect];
NSPredicate *cellAttributesPredicate = [NSPredicate predicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject,
NSDictionary<NSString *,id> * _Nullable bindings)
{
return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell);
}];
NSArray *cellAttributes = [attributes filteredArrayUsingPredicate: cellAttributesPredicate];
UICollectionViewLayoutAttributes *currentAttributes;
for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
{
currentAttributes = layoutAttributes;
offSetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
CGFloat nextOffset = proposedContentOffset.x + offSetAdjustment;
proposedContentOffset.x = nextOffset;
CGFloat deltaX = proposedContentOffset.x - self.collectionView.contentOffset.x;
CGFloat velX = velocity.x;
// detection form gist.github.com/rkeniger/7687301
// based on http://stackoverflow.com/a/14291208/740949
if (fabs(deltaX) <= FLT_EPSILON || fabs(velX) <= FLT_EPSILON || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0))
{
}
else if (velocity.x > 0.0)
{
// revert the array to get the cells from the right side, fixes not correct center on different size in some usecases
NSArray *revertedArray = [[array reverseObjectEnumerator] allObjects];
BOOL found = YES;
float proposedX = 0.0;
for (UICollectionViewLayoutAttributes *layoutAttributes in revertedArray)
{
if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (itemHorizontalCenter > proposedContentOffset.x) {
found = YES;
proposedX = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
} else {
break;
}
}
}
// dont set on unfound element
if (found) {
proposedContentOffset.x = proposedX;
}
}
else if (velocity.x < 0.0)
{
for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (itemHorizontalCenter > proposedContentOffset.x)
{
proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
break;
}
}
}
proposedContentOffset.y = 0.0;
return proposedContentOffset;
}
refer to this answer by Dan Abramov here's Swift version
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
var _proposedContentOffset = CGPoint(
x: proposedContentOffset.x, y: proposedContentOffset.y
)
var offSetAdjustment: CGFloat = CGFloat.greatestFiniteMagnitude
let horizontalCenter: CGFloat = CGFloat(
proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0)
)
let targetRect = CGRect(
x: proposedContentOffset.x,
y: 0.0,
width: self.collectionView!.bounds.size.width,
height: self.collectionView!.bounds.size.height
)
let array: [UICollectionViewLayoutAttributes] =
self.layoutAttributesForElements(in: targetRect)!
as [UICollectionViewLayoutAttributes]
for layoutAttributes: UICollectionViewLayoutAttributes in array {
if layoutAttributes.representedElementCategory == UICollectionView.ElementCategory.cell {
let itemHorizontalCenter: CGFloat = layoutAttributes.center.x
if abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment) {
offSetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
}
var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment
repeat {
_proposedContentOffset.x = nextOffset
let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x
let velX = velocity.x
if
deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) ||
(velX < 0.0 && deltaX < 0.0)
{
break
}
if velocity.x > 0.0 {
nextOffset = nextOffset + self.snapStep()
} else if velocity.x < 0.0 {
nextOffset = nextOffset - self.snapStep()
}
} while self.isValidOffset(offset: nextOffset)
_proposedContentOffset.y = 0.0
return _proposedContentOffset
}
func isValidOffset(offset: CGFloat) -> Bool {
return (offset >= CGFloat(self.minContentOffset()) &&
offset <= CGFloat(self.maxContentOffset()))
}
func minContentOffset() -> CGFloat {
return -CGFloat(self.collectionView!.contentInset.left)
}
func maxContentOffset() -> CGFloat {
return CGFloat(
self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width
)
}
func snapStep() -> CGFloat {
return self.itemSize.width + self.minimumLineSpacing
}
or gist here https://gist.github.com/katopz/8b04c783387f0c345cd9
Here is my Swift solution on a horizontally scrolling collection view. It's simple, sweet and avoids any flickering.
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return proposedContentOffset }
let currentXOffset = collectionView.contentOffset.x
let nextXOffset = proposedContentOffset.x
let maxIndex = ceil(currentXOffset / pageWidth())
let minIndex = floor(currentXOffset / pageWidth())
var index: CGFloat = 0
if nextXOffset > currentXOffset {
index = maxIndex
} else {
index = minIndex
}
let xOffset = pageWidth() * index
let point = CGPointMake(xOffset, 0)
return point
}
func pageWidth() -> CGFloat {
return itemSize.width + minimumInteritemSpacing
}
a small issue I encountered while using targetContentOffsetForProposedContentOffset is a problem with the last cell not adjusting according to the new point I returned.
I found out that the CGPoint I returned had a Y value bigger then allowed so i used the following code at the end of my targetContentOffsetForProposedContentOffset implementation:
// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
newY = maxY;
}
return CGPointMake(0, newY);
just to make it clearer this is my full layout implementation which just imitates vertical paging behavior:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
return [self targetContentOffsetForProposedContentOffset:proposedContentOffset];
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGFloat heightOfPage = self.itemSize.height;
CGFloat heightOfSpacing = self.minimumLineSpacing;
CGFloat numOfPage = lround(proposedContentOffset.y / (heightOfPage + heightOfSpacing));
CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);
// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
newY = maxY;
}
return CGPointMake(0, newY);
}
hopefully this will save someone some time and a headache
I prefer to allow user flicking through several pages. So here is my version of targetContentOffsetForProposedContentOffset (which based on DarthMike answer) for vertical layout.
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGFloat approximatePage = self.collectionView.contentOffset.y / self.pageHeight;
CGFloat currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage);
NSInteger flickedPages = ceil(velocity.y / self.flickVelocity);
if (flickedPages) {
proposedContentOffset.y = (currentPage + flickedPages) * self.pageHeight;
} else {
proposedContentOffset.y = currentPage * self.pageHeight;
}
return proposedContentOffset;
}
- (CGFloat)pageHeight {
return self.itemSize.height + self.minimumLineSpacing;
}
- (CGFloat)flickVelocity {
return 1.2;
}
Fogmeisters answer worked for me unless I scrolled to the end of the row. My cells don't fit neatly on the screen so it would scroll to the end and jump back with a jerk so that the last cell always overlapped the right edge of the screen.
To prevent this add the following line of code at the start of the targetcontentoffset method
if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
return proposedContentOffset;
#André Abreu's Code
Swift3 version
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
Swift 4
The easiest solution for collection view with cells of one size (horizontal scroll):
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return proposedContentOffset }
// Calculate width of your page
let pageWidth = calculatedPageWidth()
// Calculate proposed page
let proposedPage = round(proposedContentOffset.x / pageWidth)
// Adjust necessary offset
let xOffset = pageWidth * proposedPage - collectionView.contentInset.left
return CGPoint(x: xOffset, y: 0)
}
func calculatedPageWidth() -> CGFloat {
return itemSize.width + minimumInteritemSpacing
}
A shorter solution (assuming you're caching your layout attributes):
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0)
}
To put this in context:
class Layout : UICollectionViewLayout {
private var cache: [UICollectionViewLayoutAttributes] = []
private static let horizontalPadding: CGFloat = 16
private static let interItemSpacing: CGFloat = 8
override func prepare() {
let (itemWidth, itemHeight) = (collectionView!.bounds.width - 2 * Layout.horizontalPadding, collectionView!.bounds.height)
cache.removeAll()
let count = collectionView!.numberOfItems(inSection: 0)
var x: CGFloat = Layout.horizontalPadding
for item in (0..<count) {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: x, y: 0, width: itemWidth, height: itemHeight)
cache.append(attributes)
x += itemWidth + Layout.interItemSpacing
}
}
override var collectionViewContentSize: CGSize {
let width: CGFloat
if let maxX = cache.last?.frame.maxX {
width = maxX + Layout.horizontalPadding
} else {
width = collectionView!.width
}
return CGSize(width: width, height: collectionView!.height)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache.first { $0.indexPath == indexPath }
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cache.filter { $0.frame.intersects(rect) }
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0)
}
}
To make sure it works in Swift version (swift 5 now), I used the answer from #André Abreu, I add some more informations:
When subclassing UICollectionViewFlowLayout, the "override func awakeFromNib(){}" doesn't works (don't know why). Instead, I used "override init(){super.init()}"
This is my code put in class SubclassFlowLayout: UICollectionViewFlowLayout {} :
let padding: CGFloat = 16
override init() {
super.init()
self.minimumLineSpacing = padding
self.minimumInteritemSpacing = 2
self.scrollDirection = .horizontal
self.sectionInset = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: 100) //right = "should set for footer" (Horizental)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let leftInset = padding
let horizontalOffset = proposedContentOffset.x + leftInset // leftInset is for "where you want the item stop on the left"
let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
let targetPoint = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
return targetPoint
}
After subclassing, make sure to put this in ViewDidLoad():
customCollectionView.collectionViewLayout = SubclassFlowLayout()
customCollectionView.isPagingEnabled = false
customCollectionView.decelerationRate = .fast //-> this for scrollView speed
For those looking for a solution in Swift:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
private let collectionViewHeight: CGFloat = 200.0
private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width
override func awakeFromNib() {
super.awakeFromNib()
self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere])
self.minimumInteritemSpacing = [InsertItemSpacingHere]
self.scrollDirection = .Horizontal
let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2
self.collectionView?.contentInset = UIEdgeInsets(top: 0,
left: inset,
bottom: 0,
right: inset)
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.max
let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2)
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight)
var array = super.layoutAttributesForElementsInRect(targetRect)
for layoutAttributes in array! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
It is not about collectionView, but it works better.
It is the best solution I ever seen.
Just use it with .linear type.
https://github.com/nicklockwood/iCarousel
God bless the author!:)
Here is a demo for paging by cell (when scroll fast, not skip one or more cell): https://github.com/ApesTalk/ATPagingByCell