JEL JEL - 7 months ago 9
Swift Question

Dynamically change position based on scrollView

I have a "U" shaped

UIBezierPath
which I use as the
path
for my
myImage.layer
to animate on. I also have a scrollView. My goal is to have a custom "Pull to Refresh" animation.

The problem I am having is that I want my
myImage.layer
to update based on how much the scrollView scrolled.

As the scrollView is pulled down, the
myImage.layer
animates along a "U" shape
path
. This is the
path
in my code which I created as a
UIBezierPath
.

This is how I calculate how far the scrollView is pulled down:

func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)

if !isRefreshing {
redrawFromProgress(self.progress)
}
}


This is the function to dynamically update the position (it is not working):

func redrawFromProgress(progress: CGFloat) {

// PROBLEM: This is not correct. Only the `x` position is dynamic based on scrollView position.
// The `y` position is static.
// I want this to be dynamic based on how much the scrollView scrolled.
myImage.layer.position = CGPoint(x: progress, y: 50)

}


Basically, this is what I want:


  • If the scrollView scrolled is 0.0, then the
    myImage.layer
    position should be CGPoint(x: 0, y: 0) or the starting point of the
    path
    .

  • If the scrollView scrolled is 0.5 (50%), then the
    myImage.layer
    position should be at 50% of the
    path
    , I don't know what the CGPoint value would be here.

  • and so on...



I tried getting the CGPoint values along the
UIBezierPath
and based on the % of the scrollView scrolled, assign that CGPoint value to it but don't know how to do this. I also looked at this post but I can't get it to work for me.

EDIT QUESTION 1:

By using this extension, I was able to get an array of
CGPoints
which contain 10 values based on my
UIBezierPath
:

extension CGPath {
func forEachPoint(@noescape body: @convention(block) (CGPathElement) -> Void) {
typealias Body = @convention(block) (CGPathElement) -> Void
func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
let body = unsafeBitCast(info, Body.self)
body(element.memory)
}
// print(sizeofValue(body))
let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
CGPathApply(self, unsafeBody, callback)
}

func getPathElementsPoints() -> [CGPoint] {
var arrayPoints : [CGPoint]! = [CGPoint]()
self.forEachPoint { element in
switch (element.type) {
case CGPathElementType.MoveToPoint:
arrayPoints.append(element.points[0])
case .AddLineToPoint:
arrayPoints.append(element.points[0])
case .AddQuadCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
case .AddCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
arrayPoints.append(element.points[2])
default: break
}
}
return arrayPoints
}


I also rewrote the function above called
redrawFromProgress(progress: CGFloat)
to this:

func redrawFromProgress(progress: CGFloat) {

let enterPath = paths[0]
let pathPointsArray = enterPath.CGPath
let junctionPoints = pathPointsArray.getPathElementsPoints()
// print(junctionPoints.count) // There are 10 junctionPoints

// progress means how much the scrollView has been pulled down,
// it goes from 0.0 to 1.0.

if progress <= 0.1 {

myImage.layer.position = junctionPoints[0]

} else if progress > 0.1 && progress <= 0.2 {

myImage.layer.position = junctionPoints[1]

} else if progress > 0.2 && progress <= 0.3 {

myImage.layer.position = junctionPoints[2]

} else if progress > 0.3 && progress <= 0.4 {

myImage.layer.position = junctionPoints[3]

} else if progress > 0.4 && progress <= 0.5 {

myImage.layer.position = junctionPoints[4]

} else if progress > 0.5 && progress <= 0.6 {

myImage.layer.position = junctionPoints[5]

} else if progress > 0.6 && progress <= 0.7 {

myImage.layer.position = junctionPoints[6]

} else if progress > 0.7 && progress <= 0.8 {

myImage.layer.position = junctionPoints[7]

} else if progress > 0.8 && progress <= 0.9 {

myImage.layer.position = junctionPoints[8]

} else if progress > 0.9 && progress <= 1.0 {

myImage.layer.position = junctionPoints[9]

}

}


If I pull down the scrollView very slow, the
myImage.layer
actually follows the path. The only problem is that if I pull down on the scrollView very fast, then the
myImage.layer
jumps to the last point. Could it be because of the way I wrote the
if statement
above?

Any ideas?

JEL JEL
Answer

Thanks to @Sam Falconer for making me aware of this:

Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.

Once I confirmed this, he also helped by mentioning:

Additionally, you will find the CAKeyframeAnimation class to be useful.

With CAKeyfraneAnimation I am able to manually control it's value with this code:

func scrollViewDidScroll(scrollView: UIScrollView) {
    let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
    self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)

    if !isRefreshing {
        redrawFromProgress(self.progress)
    }
}


func redrawFromProgress(progress: CGFloat) {

    // Animate image along enter path
    let pathAnimation = CAKeyframeAnimation(keyPath: "position")
    pathAnimation.path = myPath.CGPath
    pathAnimation.calculationMode = kCAAnimationPaced
    pathAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
    pathAnimation.beginTime = 1e-100
    pathAnimation.duration = 1.0
    pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
    pathAnimation.removedOnCompletion = false
    pathAnimation.fillMode = kCAFillModeForwards

    imageLayer.addAnimation(pathAnimation, forKey: nil)
    imageLayer.position = enterPath.currentPoint
}

Thanks again for the help guys!

Comments