How to restrict a draggable UIView to specific bounds - ios

I have a UIImageView which I have made Draggable via a custom class called DraggableImageView2. The class is then instantiated in a UIViewController with an image and can be dragged within a UIView. The thing is I want to restrict the UIImageView not to go past the specific boundaries when dragged. Here is my current code:
import UIKit
class DraggableImageView2: UIImageView, UIGestureRecognizerDelegate {
var dragStartPositionRelativeToCenter : CGPoint?
var dragGesture: UIGestureRecognizer!
var zoomGesture: UIGestureRecognizer!
var lastKnownCenterX: CGFloat = 0.0
var lastKnownCenterY: CGFloat = 0.0
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initializeGestures()
}
override init(image: UIImage?) {
super.init(image: image)
self.initializeGestures()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.initializeGestures()
}
func initializeGestures() {
self.userInteractionEnabled = true
self.multipleTouchEnabled = true
dragGesture = UIPanGestureRecognizer(target: self, action: #selector(DraggableImageView.handlePan(_:)))
self.addGestureRecognizer(dragGesture)
}
func handlePan(recognizer: UIPanGestureRecognizer!) {
Scripts.log("PAN >>> MOVE ACTIVATED")
if recognizer.state == UIGestureRecognizerState.Began {
let locationInView = recognizer.locationInView(superview)
dragStartPositionRelativeToCenter = CGPoint(x: locationInView.x - center.x, y: locationInView.y - center.y)
return
}
if recognizer.state == UIGestureRecognizerState.Ended {
dragStartPositionRelativeToCenter = nil
return
}
let locationInView = recognizer.locationInView(superview)
Scripts.log("PAN LOCATION >>> X = \(self.frame.origin.x) | Y = \(self.frame.origin.y) | LX = \(locationInView.x) | LY = \(locationInView.y) | DX = \(self.dragStartPositionRelativeToCenter!.x) | DY = \(self.dragStartPositionRelativeToCenter!.y)")
let xDragMin: CGFloat = 1.0
let yDragMin: CGFloat = 1.0
let xDragMax: CGFloat = kDEVICE_WIDTH - self.frame.size.width - 1.0
let yDragMax: CGFloat = kDEVICE_WIDTH - self.frame.size.height - 1.0
var newCenterX: CGFloat = locationInView.x - self.dragStartPositionRelativeToCenter!.x
var newCenterY: CGFloat = locationInView.y - self.dragStartPositionRelativeToCenter!.y
if self.frame.origin.x < xDragMin {
Scripts.log("PAN LOCATION >>> X MIN PAST BOUNDS")
newCenterX = lastKnownCenterX
}
if self.frame.origin.y < yDragMin {
Scripts.log("PAN LOCATION >>> Y MIN PAST BOUNDS")
newCenterY = lastKnownCenterY
}
if self.frame.origin.x > xDragMax {
Scripts.log("PAN LOCATION >>> X MAX PAST BOUNDS")
}
if self.frame.origin.y > yDragMax {
Scripts.log("PAN LOCATION >>> Y MAX PAST BOUNDS")
}
UIView.animateWithDuration(0.1) {
self.center = CGPoint(x: newCenterX,
y: newCenterY)
}
lastKnownCenterX = newCenterX
lastKnownCenterY = newCenterY
}
func handlePinch(recognizer: UIPinchGestureRecognizer!) {
Scripts.log("PINCH >>> ZOOM ACTIVATED")
//self.bringSubviewToFront(recognizer.view!)
recognizer.view?.transform = CGAffineTransformScale((recognizer.view?.transform)!, recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
Scripts.log("shouldRecognizeSimultaneouslyWithGestureRecognizer WAS CALLED")
return true
}
}
I've only started working on the xDragMin and yDragMin portion. Everything works for stopping it when it is less than the xDragMin, but the problem becomes for some strange reason, when you try to drag out of it, it gets frozen in that X Point. Same for the yDragMin, one if reaches the min, it stops as it should, but can't drag out of the Y Point.

So after some more digging and playing around, I seem to have been able to solve my own problem, at least for the X Bounds Min. Here is what I did:
(1) Added these new variables
var isWithinXBounds: Bool = true
var lastKnownLX: CGFloat = 0.0
(2) The logic starting with "if self.frame.origin.x < xDragMin" needed to be fine tuned. This condition is perfectly fine until it hits the X bounds. Once it does then self.frame.origin.x will never change (DUH) cos now its position is locked. I need another condition to check that once the bounds has been hit i.e. locked, check if the users touch is moving in a +ve direction. If so, then set the BOOL value back to TRUE which will unlock the x position
if self.frame.origin.x < xDragMin {
Scripts.log("PAN LOCATION >>> X MIN PAST BOUNDS")
self.isWithinXBounds = false
//If TOUCH still moving in negative direction
if locationInView.x - lastKnownLX < 0 {
newCenterX = lastKnownCenterX
}
else {
self.isWithinXBounds = true
}
}
(3) Here is the condition that will then unlock the UIImageView based on the BOOL value:
if isWithinXBounds {
lastKnownCenterX = newCenterX
lastKnownLX = locationInView.x
}

Related

Determining if custom iOS views overlap

I've defined a CircleView class:
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
// Get the Graphics Context
if let context = UIGraphicsGetCurrentContext() {
// Set the circle outerline-width
context.setLineWidth(5.0);
// Set the circle outerline-colour
UIColor.blue.set()
// Create Circle
let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let radius = (frame.size.width - 10)/2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.setFillColor(UIColor.blue.cgColor)
// Draw
context.strokePath()
context.fillPath()
}
}
}
And created an array of them with a randomly set number:
var numberOfCircles: Int!
var circles: [CircleView] = []
numberOfCircles = Int.random(in: 1..<10)
let circleWidth = CGFloat(50)
let circleHeight = circleWidth
var i = 0
while i < numberOfCircles {
let circleView = CircleView(frame: CGRect(x: 0.0, y: 0.0, width: circleWidth, height: circleHeight))
circles.append(circleView)
i += 1
}
After creating the circles, I call a function, drawCircles, that will draw them on the screen:
func drawCircles(){
for c in circles {
c.frame.origin = c.frame.randomPoint
while !UIScreen.main.bounds.contains(c.frame.origin) {
c.frame.origin = CGPoint()
c.frame.origin = c.frame.randomPoint
let prev = circles.before(c)
if prev?.frame.intersects(c.frame) == true {
c.frame.origin = c.frame.randomPoint
}
}
}
for c in circles {
self.view.addSubview(c)
}
}
The while loop in the drawCircles method makes sure that no circles are placed outside of the bounds of the screen, and works as expected.
What I'm struggling with is to make sure that the circles don't overlap each other, like so:
I'm using the following methods to determine either the next
I'm using this methods to determine what the previous / next element in the array of circles:
extension BidirectionalCollection where Iterator.Element: Equatable {
typealias Element = Self.Iterator.Element
func after(_ item: Element, loop: Bool = false) -> Element? {
if let itemIndex = self.firstIndex(of: item) {
let lastItem: Bool = (index(after:itemIndex) == endIndex)
if loop && lastItem {
return self.first
} else if lastItem {
return nil
} else {
return self[index(after:itemIndex)]
}
}
return nil
}
func before(_ item: Element, loop: Bool = false) -> Element? {
if let itemIndex = self.firstIndex(of: item) {
let firstItem: Bool = (itemIndex == startIndex)
if loop && firstItem {
return self.last
} else if firstItem {
return nil
} else {
return self[index(before:itemIndex)]
}
}
return nil
}
}
This if statement, however; doesn't seem to be doing what I'm wanting; which is to make sure that if a circle intersects with another one, to change it's origin to be something new:
if prev?.frame.intersects(c.frame) == true {
c.frame.origin = c.frame.randomPoint
}
If anyone has any ideas where the logic may be, or of other ideas on how to make sure that the circles don't overlap with each other, that would be helpful!
EDIT: I did try the suggestion that Eugene gave in his answer like so, but still get the same result:
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(sqrt(xDist * xDist + yDist * yDist))
}
if prev != nil {
if distance((prev?.frame.origin)!, c.frame.origin) <= 40 {
print("2")
c.frame.origin = CGPoint()
c.frame.origin = c.frame.randomPoint
}
}
But still the same result
EDIT 2
Modified my for loop based on Eugene's edited answer / clarifications; still having issues with overlapping circles:
for c in circles {
c.frame.origin = c.frame.randomPoint
let prev = circles.before(c)
let viewMidX = self.circlesView.bounds.midX
let viewMidY = self.circlesView.bounds.midY
let xPosition = self.circlesView.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let yPosition = self.circlesView.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
if let prev = prev {
if distance(prev.center, c.center) <= 50 {
c.center = CGPoint(x: xPosition, y: yPosition)
}
}
}
That’s purely geometric challenge. Just ensure that distance between the circle centers greater than or equal to sum of their radiuses.
Edit 1
Use UIView.center instead of UIView.frame.origin. UIView.frame.origin gives you the top left corner of UIView.
if let prev = prev {
if distance(prev.center, c.center) <= 50 {
print("2")
c.center = ...
}
}
Edit 2
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(hypot(xDist, yDist))
}
let prev = circles.before(c)
if let prevCircleCenter = prev?.center {
let distance = distance(prevCenter, c.center)
if distance <= 50 {
let viewMidX = c.bounds.midX
let viewMidY = c.bounds.midY
var newCenter = c.center
var centersVector = CGVector(dx: newCenter.x - prevCircleCenter.x, dy: newCenter.y - prevCircleCenter.y)
centersVector.dx *= 51 / distance
centersVector.dy *= 51 / distance
newCenter.x = prevCircleCenter.x + centersVector.dx
newCenter.y = prevCircleCenter.y + centersVector.dy
c.center = newCenter
}
}

Running swift condition just once on translation

I created a UIView and a UIImageView which is inside the UIView as a subview, then I added a pan gesture to the UIImageView to slide within the UIView, the image slides now but the problem I have now is when the slider gets to the end of the view if movex > xMax, I want to print this just once print("SWIPPERD movex"). The current code I have there continues to print print("SWIPPERD movex") as long as the user does not remove his/her hand from the UIImageView which is used to slide
private func swipeFunc() {
let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(acknowledgeSwiped(sender:)))
sliderImage.addGestureRecognizer(swipeGesture)
swipeGesture.delegate = self as? UIGestureRecognizerDelegate
}
#objc func acknowledgeSwiped(sender: UIPanGestureRecognizer) {
if let sliderView = sender.view {
let translation = sender.translation(in: self.baseView) //self.sliderView
switch sender.state {
case .began:
startingFrame = sliderImage.frame
viewCenter = baseView.center
fallthrough
case .changed:
if let startFrame = startingFrame {
var movex = translation.x
if movex < -startFrame.origin.x {
movex = -startFrame.origin.x
print("SWIPPERD minmax")
}
let xMax = self.baseView.frame.width - startFrame.origin.x - startFrame.width - 15 //self.sliderView
if movex > xMax {
movex = xMax
print("SWIPPERD movex")
}
var movey = translation.y
if movey < -startFrame.origin.y { movey = -startFrame.origin.y }
let yMax = self.baseView.frame.height - startFrame.origin.y - startFrame.height //self.sliderView
if movey > yMax {
movey = yMax
// print("SWIPPERD min")
}
sliderView.transform = CGAffineTransform(translationX: movex, y: movey)
}
default: // .ended and others:
UIView.animate(withDuration: 0.1, animations: {
sliderView.transform = CGAffineTransform.identity
})
}
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return sliderImage.frame.contains(point)
}
You may want to use the .ended state instead of .changed state, based on your requirements. And you've mentioned you want to get the right direction only. You could try below to determine if the swipe came from right to left, or vice-versa, change as you wish:
let velocity = sender.velocity(in: sender.view)
let rightToLeftSwipe = velocity.x < 0

Get absolute position relative to screen of SKEffectNode as it is warped with SKWarpGeometryGrid

I have an SKVideoNode as a child of an SKEffectNode which enables me to skew the video using a 1 row x 1 column SKWarpGeometryGrid.
As you drag your finger in each quadrant, new corner coordinates are calculated in touchMoved(toPoint fingerPos: CGPoint) and the SKWarpGeometryGrid is then updated in updateGrid()
I also have a blue square centred on the SKView (and therefore on the video’s initial centre). I would like the blue square to follow the videoNode’s centre as the effectNode warps.
I do not want the square added as a child to the effectNode as the square would get warped as well.
I’ve tried various parameter permutations of convert(:to:) & convert(:from:)
e.g.
blueSquare?.position = self.convert(effectNode!.position, to: self)
… but the blue square never moves. Any thoughts?
code: (replace myFriend.mp4 with a video file to test)
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let warpGeometryGrid1Col = SKWarpGeometryGrid(columns: 1,rows: 1)
var warpGridPositions: [float2] = [
float2(0.0, 0.0), float2(1.0, 0.0),
float2(0.0, 1.0), float2(1.0, 1.0)]
var adjust: Float = 0.005
var previousPosX: CGFloat = 0
var previousPosY: CGFloat = 0
var topLeftX:Float = 0.0
var topLeftY:Float = 1.0
var topRightX:Float = 1.0
var topRightY:Float = 1.0
var bottomLeftX:Float = 0.0
var bottomLeftY:Float = 0.0
var bottomRightX:Float = 1.0
var bottomRightY:Float = 0.0
var blueSquare:SKSpriteNode?
var effectNode: SKEffectNode?
override func didMove(to view: SKView) {
// **** add a video file to the project
let videoNode = SKVideoNode.init(fileNamed: "myFriend.mp4")
videoNode.size.width = self.frame.size.height/3*1.5
videoNode.size.height = self.frame.size.width/3*1.5
videoNode.play()
effectNode = SKEffectNode()
effectNode!.addChild(videoNode)
self.addChild(effectNode!)
blueSquare = SKSpriteNode.init(color: .blue, size: CGSize(width: 50, height: 50))
self.addChild(blueSquare!)
}
func updateGrid(){
warpGridPositions = [float2(bottomLeftX, bottomLeftY), float2(bottomRightX, bottomRightY),
float2(topLeftX, topLeftY), float2(topRightX, topRightY)]
effectNode!.warpGeometry = warpGeometryGrid1Col.replacingByDestinationPositions(positions: warpGridPositions)
//*********************************
//*********************************
// TRY TO ADJUST POSITION OF blueSquare AS EFFECT
// NODE IS WARPED KEEPING IT CENTERED ON THE
// EFFECT NODE'S CHILD VIDEO
blueSquare?.position = self.convert(effectNode!.position, to: self)
//*********************************
//*********************************
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
func touchMoved(toPoint fingerPos : CGPoint) {
if fingerPos.y < 0{
if fingerPos.x > 0{// bottom right
if fingerPos.x > previousPosX {
bottomRightX += adjust
}
if fingerPos.x < previousPosX {
bottomRightX -= adjust
}
if fingerPos.y > previousPosY {
bottomRightY += adjust
}
if fingerPos.y < previousPosY {
bottomRightY -= adjust
}
}
else{ // bottom left
if fingerPos.x > previousPosX {
bottomLeftX += adjust
}
if fingerPos.x < previousPosX {
bottomLeftX -= adjust
}
if fingerPos.y > previousPosY {
bottomLeftY += adjust
}
if fingerPos.y < previousPosY {
bottomLeftY -= adjust
}
}
} else{
// top right
if fingerPos.x > 0{
if fingerPos.x > previousPosX {
topRightX += adjust
}
if fingerPos.x < previousPosX {
topRightX -= adjust
}
if fingerPos.y > previousPosY {
topRightY += adjust
}
if fingerPos.y < previousPosY {
topRightY -= adjust
}
}
else{ // top left
if fingerPos.x > previousPosX {
topLeftX += adjust
}
if fingerPos.x < previousPosX {
topLeftX -= adjust
}
if fingerPos.y > previousPosY {
topLeftY += adjust
}
if fingerPos.y < previousPosY {
topLeftY -= adjust
}
}
}
updateGrid()
previousPosX = fingerPos.x
previousPosY = fingerPos.y
}
}
If effectNode is starting from the center of SKScene, the calculation is direct. The viewFrame could to be cached for accelerate calculation.
var videoFrame : CGRect!
override func didMove(to view: SKView) {
// **** add a video file to the project
let videoNode = SKVideoNode.init(fileNamed: "myFriend.mp4")
videoNode.size.width = self.frame.size.height/3*1.5
videoNode.size.height = self.frame.size.width/3*1.5
videoNode.play()
videoFrame = (videoNode.frame)
effectNode = SKEffectNode()
effectNode!.addChild(videoNode)
self.addChild(effectNode!)
blueSquare = SKSpriteNode.init(color: .blue, size: CGSize(width: 50, height: 50))
self.addChild(blueSquare!)
}
func updateGrid(){
warpGridPositions = [float2(bottomLeftX, bottomLeftY), float2(bottomRightX, bottomRightY),
float2(topLeftX, topLeftY), float2(topRightX, topRightY)]
effectNode!.warpGeometry = warpGeometryGrid1Col.replacingByDestinationPositions(positions: warpGridPositions)
let finalRatio = ( warpGridPositions.reduce(float2(),+) / 4.0)
let finalWidth = videoFrame.origin.x + CGFloat(finalRatio.x) * videoFrame.size.width
let finalHeight = videoFrame.origin.y + CGFloat(finalRatio.y) * videoFrame.size.height
blueSquare?.position = CGPoint.init(x:finalWidth , y: finalHeight)
}
Hope there is no problem.
It's for the blue square not in the center. As there is nonlinear twist close to edges, the result is better when blue square is within a center region of video.
override func didMove(to view: SKView) {
// **** add a video file to the project
let videoNode = SKVideoNode.init(fileNamed: "myFriend.mp4")
videoNode.size.width = self.frame.size.height/3*1.5
videoNode.size.height = self.frame.size.width/3*1.5
videoNode.play()
videoFrame = (videoNode.frame)
effectNode = SKEffectNode()
effectNode!.addChild(videoNode)
self.addChild(effectNode!)
blueSquare = SKSpriteNode.init(color: .blue, size: CGSize(width: 50, height: 50))
self.addChild(blueSquare!)
blueSquare?.position = CGPoint.init(x: 100, y: 100)
cachedValue = cacheFrame(relativePosition((blueSquare?.position)!, videoFrame))
}
var cachedValue : simd_quatd!
func relativePosition(_ point: CGPoint, _ frame: CGRect) -> CGPoint{
return CGPoint.init(x: (point.x - frame.origin.x) / frame.size.width , y:
(point.y - frame.origin.y) / frame.size.height
)
}
func cacheFrame(_ point : CGPoint)-> simd_quatd {
let x = Double(point.x)
let y = Double(point.y)
return simd_quatd(vector: double4([(1-x)*(1-y), x*(1-y), (1-x)*y, x*y]))
}
func updateGrid(){
warpGridPositions = [float2(bottomLeftX, bottomLeftY), float2(bottomRightX, bottomRightY),
float2(topLeftX, topLeftY), float2(topRightX, topRightY)]
effectNode!.warpGeometry = warpGeometryGrid1Col.replacingByDestinationPositions(positions: warpGridPositions)
let finalRatio = CGPoint(x: CGFloat( simd_dot( simd_quatd(vector:
double4( warpGridPositions.map{Double($0.x)})) ,
cachedValue )),y:
CGFloat( simd_dot(simd_quatd(vector:
double4( warpGridPositions.map{Double($0.y)})) , cachedValue))
)
let finalWidth = videoFrame.origin.x + CGFloat(finalRatio.x) * videoFrame.size.width
let finalHeight = videoFrame.origin.y + CGFloat(finalRatio.y) * videoFrame.size.height
blueSquare?.position = CGPoint.init(x:finalWidth , y: finalHeight)
}

UIImageView disappears after rotate and move to edge

I have written a custom class which offers the feature to move and rotates images.
I need to restrict the movement to the boundaries of parent view or Superview.
So, I wrote below code to restrict it.
This works fine before an image is rotated. If I try to rotate the image and then move the image to an edge, Image disappears leaving no log or traces.
Why does it disappear and how do I avoid it?
if(frame.origin.x < 1)
{
frame.origin.x = 1
}
if(frame.origin.y < 1)
{
frame.origin.y = 1
}
if(frame.maxX > superview!.frame.width)
{
frame.origin.x = superview!.frame.width - frame.width - 1
}
if(frame.maxY > superview!.frame.height)
{
frame.origin.y = superview!.frame.height - frame.height - 1
}
If I remove the above code, nothing disappears but image moves out of boundaries. So I feel something wrong in only above lines.
So help me to correctly implement this feature after rotation.
Full Movable Image Class code :
class movableImageView: UIImageView
{
var CenCooVar = CGPoint()
override init(image: UIImage!)
{
super.init(image: image)
self.userInteractionEnabled = true
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(moveImageFnc(_:)))
let rotateImage = UIRotationGestureRecognizer(target: self, action: #selector(rotateImageFnc(_:)))
self.gestureRecognizers = [moveImage,rotateImage]
}
func moveImageFnc(moveImage: UIPanGestureRecognizer)
{
if moveImage.state == UIGestureRecognizerState.Began
{
CenCooVar = self.center
}
if moveImage.state == UIGestureRecognizerState.Changed
{
let moveCooVar = moveImage.translationInView(self.superview!)
self.center = CGPoint(x: CenCooVar.x + moveCooVar.x, y: CenCooVar.y + moveCooVar.y)
if(frame.origin.x < 1)
{
frame.origin.x = 1
}
if(frame.origin.y < 1)
{
frame.origin.y = 1
}
if(frame.maxX > superview!.frame.width)
{
frame.origin.x = superview!.frame.width - frame.width - 1
}
if(frame.maxY > superview!.frame.height)
{
frame.origin.y = superview!.frame.height - frame.height - 1
}
}
if moveImage.state == UIGestureRecognizerState.Ended
{
CenCooVar = self.center
}
}
func rotateImageFnc(rotateImage: UIRotationGestureRecognizer)
{
if rotateImage.state == UIGestureRecognizerState.Changed
{
self.transform = CGAffineTransformRotate(self.transform, rotateImage.rotation)
rotateImage.rotation = 0
}
}
}

Why collectionView cell centering works only in one direction?

I'm trying to centerelize my cells on horizontal scroll. I've written one method, but it works only when I scroll to right, on scroll on left it just scrolls, without stopping on the cell's center.
Can anyone help me to define this bug, please?
class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfWidth = cvBounds.size.width * 0.5;
let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth;
if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(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.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) {
continue
}
// == First time in the loop == //
guard let candAttrs = candidateAttributes else {
candidateAttributes = attributes
continue
}
let a = attributes.center.x - proposedContentOffsetCenterX
let b = candAttrs.center.x - proposedContentOffsetCenterX
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
if(proposedContentOffset.x == -(cv.contentInset.left)) {
return proposedContentOffset
}
return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y)
}
} else {
print("else")
}
// fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
And in my UIViewController:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var insets = self.collectionView.contentInset
let value = ((self.view.frame.size.width - ((CGRectGetWidth(collectionView.frame) - 35))) * 0.5)
insets.left = value
insets.right = value
self.collectionView.contentInset = insets
self.collectionView.decelerationRate = UIScrollViewDecelerationRateNormal
}
If you have any question - please ask me
I actually just did a slightly different implementation for another thread, but adjusted it to work for this questions. Try out the solution below :)
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
// Tracks the currently visible index
private var visibleIndex : Int = 0
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enough in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}

Resources