Scale UITextView with pan gesture - swift

I'm making a feature where user can add text sticker, scale and rotate it using pan gesture by dragging on right bottom corner.
I made a custom class inherited from UIView, I put UITextView on it, 3 views on corners for buttons(close, edit and zoom/rotate) and transparent UIView on top for gestures.
Sorry, I have no enough reputation to add images
Text Sticker view structure - https://imgur.com/lDz47fV
My attempt is to use CGAffineTransform on pan gesture. Pan gesture is lying on Rotate Button View (bottom left corner)
#IBAction func resizeGestureRecognizer(_ sender: UIPanGestureRecognizer) {
let touchLocation = sender.location(in: self.superview)
let center = self.center
switch sender.state {
case .began:
self.deltaAngle = CGFloat(atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))) - CGAffineTransformGetAngle(self.transform)
self.initialDistance = CGPointGetDistance(point1: center, point2: touchLocation)
//self.initialBounds = self.bounds
case .changed:
let angle = atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))
let angleDiff = Float(self.deltaAngle) - angle
let scale = CGPointGetDistance(point1: center, point2: touchLocation) / self.initialDistance
// downscale buttons to ramain same size and rotation
for button in buttonViews {
var t = CGAffineTransform.identity
t = t.rotated(by: CGFloat(angleDiff))
t = t.scaledBy(x: 1/scale, y: 1/scale)
button.transform = t
}
var t = CGAffineTransform.identity
t = t.rotated(by: CGFloat(-angleDiff))
t = t.scaledBy(x: scale, y: scale)
self.transform = t
// textView.layer.shouldRasterize = true
// textView.layer.rasterizationScale = 20
case .ended, .cancelled:
print("ended")
default:
break
}
}
This code works but every time I'm trying to resize or rotate sticker it scale back to original scale. What am I missing?
Result - https://imgur.com/QHPlI93
Also, as a workaround for scaled font quality, I'm using huge fornt size(100) and scale of 0.5 for sticker:
override func awakeFromNib() {
textView.textContainerInset = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
frameView.layer.cornerRadius = 13
showEditingHandlers = true
textView.becomeFirstResponder()
let scale: CGFloat = 0.5
for button in buttonViews {
var t = CGAffineTransform.identity
t = t.scaledBy(x: 1/scale, y: 1/scale)
button.transform = t
}
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: scale)
self.transform = t
textView.delegate = self
update()
}

Ok, in case someone else will be searching for similar problem, I'll post my solution
I added variables fro scale in top of my class:
var scale: CGFloat = 0
var savedScale: CGFloat = 0.5 // 0.5 becuase I use double font size and scaled my sticker by 0.5 to handle font blur
next, in my gesture handle func I just save last scale like this:
#IBAction func resizeGestureRecognizer(_ sender: UIPanGestureRecognizer) {
let touchLocation = sender.location(in: self.superview)
let center = self.center
switch sender.state {
case .began:
print("saved scale = \(savedScale)")
self.deltaAngle = CGFloat(atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))) - CGAffineTransformGetAngle(self.transform)
self.initialDistance = CGPointGetDistance(point1: center, point2: touchLocation)
case .changed:
let angle = atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))
let angleDiff = Float(self.deltaAngle) - angle
scale = CGPointGetDistance(point1: center, point2: touchLocation) / self.initialDistance
print("scale: \(scale)")
scale = scale * savedScale // <- added this
// downscale buttons to ramain same size and rotation
for button in buttonViews {
var t = CGAffineTransform.identity
//t = t.rotated(by: CGFloat(angleDiff))
t = t.scaledBy(x: 1/scale, y: 1/scale)
button.transform = t
}
var t = CGAffineTransform.identity
t = t.rotated(by: CGFloat(-angleDiff))
t = t.scaledBy(x: scale, y: scale)
self.transform = t
case .ended, .cancelled:
print("ended")
savedScale = 1 * scale // <- and this
default:
break
}
}

Related

Setup UIPanGestureRecognizer for both direction

I'm having a container view and added a UIPanGestureRecognizer on it. It works like this image:
With the UIPanGestureRecognizer it's possible to swipe to the right, and it works great
#objc func respondToSwipeRight(recognizer: UIPanGestureRecognizer) {
container.setAnchorPoint(anchorPoint: .init(x: 1, y: 1))
let touchLocation = recognizer.location(in: container.superview)
let center = container.center
switch recognizer.state{
case .began :
self.deltaAngle = atan2(touchLocation.y - center.y, touchLocation.x - center.x) - atan2(container.transform.b, container.transform.a)
case .changed:
backgroundView.backgroundColor = UIColor.systemGreen
let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
let angleDiff = self.deltaAngle - angle
if angleDiff <= 0, angleDiff > -5, angleDiff > -0.50 {
container.transform = CGAffineTransform(rotationAngle: -angleDiff)
}
case .ended:
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let this = self else { return }
this.container.transform = CGAffineTransform(rotationAngle: 0)
}, completion: { [weak self] _ in
guard let this = self else { return }
this.container.setAnchorPoint(anchorPoint: .init(x: 0.5, y: 0.5))
})
default: break
}
}
Now, I want to have the same things from right to left, I added a new UIPanGestureRecognizer to that view, but appearance I only can have one UIPanGestureRecognizer per view and it uses the latest one.
Could anyone help me to have the same mechanism for right to left? For right to left, the container anchor point should be like that:
container.setAnchorPoint(anchorPoint: .init(x: 0, y: 1))
Your help will be appreciated
Here's an example of using a single UIPanGestureRecognizer to rotate the box around either the bottom-left or bottom-right corner as the user moves their finger right and left. Be sure to start your pan gesture on the box.
Start with a new iOS app project. Use the following for the ViewController class. No other changes to the project are needed. Run on your favorite iOS device or simulator.
enum PanDirection {
case unknown
case left
case right
}
class ViewController: UIViewController {
var box: UIView!
var direction: PanDirection = .unknown
override func viewDidLoad() {
super.viewDidLoad()
let container = UIView(frame: .zero)
container.backgroundColor = .systemBlue
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
// Just some example constraints to put the box on the screen
NSLayoutConstraint.activate([
container.heightAnchor.constraint(equalToConstant: 250),
container.widthAnchor.constraint(equalTo: container.heightAnchor),
container.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
container.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
])
// The box that will be rotated
// Sized to match the blue container
box = UIView(frame: container.bounds)
box.backgroundColor = .systemGreen
box.autoresizingMask = [.flexibleWidth, .flexibleHeight]
container.addSubview(box)
// Setup the pan gesture and add to the blue container
let pan = UIPanGestureRecognizer(target: self, action: #selector(panHandler))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
container.addGestureRecognizer(pan)
}
#objc func panHandler(_ gesture: UIPanGestureRecognizer) {
if gesture.state == .began || gesture.state == .changed {
// Relative offset from the start of the gesture
let offset = gesture.translation(in: gesture.view)
// Determine which direction we are panning
if offset.x == 0 {
direction = .unknown
} else {
direction = offset.x > 0 ? .right : .left
}
if direction == .right {
// How far right we have panned
let right = offset.x
// Only rotate up to 75 degrees - just an example
let angleDeg = min(right / box.bounds.size.width * 90, 75)
// Calculate the transform. We need to translate the box to the bottom-right corner,
// then rotate, then translate back to the center.
let shift = CGAffineTransform(translationX: box.bounds.size.width / 2, y: box.bounds.size.height / 2)
box.transform = shift.rotated(by: angleDeg / 180 * .pi).translatedBy(x: -box.bounds.size.width / 2, y: -box.bounds.size.height / 2)
} else if direction == .left {
// How far to the left we have panned
let left = offset.x
// Only rotate up to 75 degrees - just an example
let angleDeg = max(left / box.bounds.size.width * 90, -75)
// Calculate the transform. We need to translate the box to the bottom-left corner,
// then rotate, then translate back to the center.
let shift = CGAffineTransform(translationX: -box.bounds.size.width / 2, y: box.bounds.size.height / 2)
box.transform = shift.rotated(by: angleDeg / 180 * .pi).translatedBy(x: box.bounds.size.width / 2, y: -box.bounds.size.height / 2)
}
} else {
// Reset for next pan gesture
direction = .unknown
}
}
}
Start a pan in the box - move right and left and the green box rotates back and forth around the appropriate corner as the finger moves.

CABasicAnimation to emulate a 'pulse' effect animation on a non-circle shape

I am using CBasicAnimation to create a pulsating effect on a button.
The effect pulses out the shape of a UIView, with border only.
While the animation works properly, I am not getting the desired effect using CABasicAnimation(keyPath: "transform.scale").
I am using an animation group with 3 animations: borderWidth, transform.scale and opacity.
class Pulsing: CALayer {
var animationGroup = CAAnimationGroup()
var initialPulseScale:Float = 1
var nextPulseAfter:TimeInterval = 0
var animationDuration:TimeInterval = 1.5
var numberOfPulses:Float = Float.infinity
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init (numberOfPulses:Float = Float.infinity, position:CGPoint, pulseFromView:UIView, rounded: CGFloat) {
super.init()
self.borderColor = UIColor.black.cgColor
self.contentsScale = UIScreen.main.scale
self.opacity = 1
self.numberOfPulses = numberOfPulses
self.position = position
self.bounds = CGRect(x: 0, y: 0, width: pulseFromView.frame.width, height: pulseFromView.frame.height)
self.cornerRadius = rounded
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
self.setupAnimationGroup(view: pulseFromView)
DispatchQueue.main.async {
self.add(self.animationGroup, forKey: "pulse")
}
}
}
func borderWidthAnimation() -> CABasicAnimation {
let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
widthAnimation.fromValue = 2
widthAnimation.toValue = 0.5
widthAnimation.duration = animationDuration
return widthAnimation
}
func createScaleAnimation (view:UIView) -> CABasicAnimation {
let scale = CABasicAnimation(keyPath: "transform.scale")
DispatchQueue.main.async {
scale.fromValue = view.layer.value(forKeyPath: "transform.scale")
}
scale.toValue = NSNumber(value: 1.1)
scale.duration = 1.0
scale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return scale
}
func createOpacityAnimation() -> CABasicAnimation {
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.duration = animationDuration
opacityAnimation.fromValue = 1
opacityAnimation.toValue = 0
opacityAnimation.fillMode = .removed
return opacityAnimation
}
func setupAnimationGroup(view:UIView) {
self.animationGroup = CAAnimationGroup()
self.animationGroup.duration = animationDuration + nextPulseAfter
self.animationGroup.repeatCount = numberOfPulses
self.animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default)
self.animationGroup.animations = [createScaleAnimation(view: view), borderWidthAnimation(), createOpacityAnimation()]
}
}
class ViewController: UIViewController {
#IBOutlet weak var pulsingView: UIView!
let roundd:CGFloat = 20
override func viewDidLoad() {
super.viewDidLoad()
pulsingView.layer.cornerRadius = roundd
let pulse = Pulsing(
numberOfPulses: .greatestFiniteMagnitude,
position: CGPoint(x: pulsingView.frame.width/2,
y: pulsingView.frame.height/2)
, pulseFromView: pulsingView, rounded: roundd)
pulse.zPosition = -10
self.pulsingView.layer.insertSublayer(pulse, at: 0)
}
}
My problem is transform.scale is maintaining the aspect ratio of the UIView it's pulsating from during the animation.
How can I make the pulse grow so there's uniform spacing on both the height and the width? See screenshot.
Scaling the width and height by the same factor is going to result in unequal spacing around the edges. You need to increase the layer's width and height by the same value. This is an addition operation, not multiplication. Now, for this pulsating effect you need to animate the layer's bounds.
If you want the spacing between the edges to be dynamic, then pick a scale factor and apply it to a single dimension. Whether you choose the width or the the height doesn't matter so long as it's only applied to one. Let's say you choose the width to grow by a factor of 1.1. Compute your target width, then compute the delta.
let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width
Once you have your delta, apply it to the layer's bounds in the x and the y dimension. Take advantage of the insetBy(dx:) method to compute the resulting rectangle.
let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)
For clarity's sake, I've renamed your createScaleAnimation(view:) method to createExpansionAnimation(view:). Tying it all together we have:
func createExpansionAnimation(view: UIView) -> CABasicAnimation {
let anim = CABasicAnimation(keyPath: "bounds")
DispatchQueue.main.async {
let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width
let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)
anim.duration = 1.0
anim.fromValue = NSValue(cgRect: self.bounds)
anim.toValue = NSValue(cgRect: targetBounds)
}
return anim
}

UIViewPropertyAnimator's bounce effect

Let's say I have an animator that moves a view from (0, 0) to (-120, 0):
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.8)
animator.addAnimations {
switch state:
case .normal: view.frame.origin.x = 0
case .swiped: view.frame.origin.x = -120
}
}
I use it together with UIPanGestureRecognizer, so that I can resize the view continuously along with the finger movements.
The issue comes when I want to add some sort of bouncing effect at the start or at the end of the animation. NOT just the damping ratio, but the bounce effect. The easiest way to imagine this is Swipe-To-Delete feature of UITableViewCell, where you can drag "Delete" button beyond its actual width, and then it bounces back.
Effectively what I want to achieve, is the way to set fractionComplete property outside of [0, 1] segment, so when the fraction is 1.2, the offset becomes 144 instead of its 120 maximum.
And right now the maximum value for fractionComplete is exactly 1.
Below are some examples to have this issue visualized:
What I currently have:
What I want to achieve:
EDIT (19 January):
Sorry for my delayed reply. Here are some clarifications:
I don't use UIView.animate(...), and use UIViewPropertyAnimator instead for a very specific reason: it handles for me all the timings, curves and velocities.
For example, you dragged the view halfway through. This means that duration of the remaining part should be two times less than total duration. Or if you dragged though the 99% of the distance, it should complete the remaining part almost instantly.
As an addition, UIViewPropertyAnimator has such features as pause (when user starts dragging once again), or reverse (when user started dragging to the left, but after that he changed his mind and moved the finger to the right), that I also benefit from.
All this is not available for simple UIView animations, or requires TONS of effort at best. It is only capable of simple transitions, and this is not the case.
That's why I have to use some sort of animator.
And as I mentioned in the comments thread in the answer that was removed by its publisher, the most complex part for me here is to simulate the friction effect: the further you drag, the less the view actually moves. Just as when you're trying to drag any UIScrollView outside of it's content.
Thanks for your effort guys, but I don't think any of these 2 answers is relevant. I will try to implement this behaviour using UIDynamicAnimator whenever I have time. Probably in the nearest week or two. I will publish my approach in case I have any decent results.
EDIT (20 January):
I just uploaded a demo project to the GitHub, which includes all the transitions that I have in my project. So now you can actually have an idea why do I need to use animators and how I use them: https://github.com/demon9733/bouncingview-prototype
The only file you are actually interested in is MainViewController+Modes.swift. Everything related to transitions and animations is contained there.
What I need to do is to enable user to drag the handle area beyond "Hide" button width with a damping effect. "Hide" button will appear on swiping the handle area to the left.
P.S. I didn't really test this demo, so it can have bugs that I don't have in my main project. So you can safely ignore them.
you need to allow pan gesture to get to needed x position and at the end of pan an animation is needed to be triggered
one way to do this would be:
var initial = CGRect.zero
override func viewDidLayoutSubviews() {
initial = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initial
let open = initial.offsetBy(dx: -120, dy: 0)
// 1 manage panning along x direction
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 2 animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = closed
})
} else {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = open
})
}
}
}
Edit 20 Jan
For simulating dampening effect and make use of UIViewPropertyAnimator specifically,
var initialOrigin = CGRect.zero
override func viewDidLayoutSubviews() {
initialOrigin = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initialOrigin
let open = initialOrigin.offsetBy(dx: -120, dy: 0)
// 1. to simulate dampening
var multiplier: CGFloat = 1.0
if animatedView?.frame.origin.x ?? CGFloat(0) > closed.origin.x || animatedView?.frame.origin.x ?? CGFloat(0) < open.origin.x {
multiplier = 0.2
} else {
multiplier = 1
}
// 2. animate panning
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x * multiplier, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 3. animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = closed.origin.x
})
animate.startAnimation()
} else {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = open.origin.x
})
animate.startAnimation()
}
}
}
Here is possible approach (simplified & a bit scratchy - only bounce, w/o button at right, because it would much more code and actually only a matter of frames management)
Due to long delay of UIPanGestureRecognizer at ending, I prefer to use UILongPressGestureRecognizer, as it gives faster feedback.
Here is demo result
The Storyboard of used below ViewController has only gray-background-rect-container view, everything else is done in code provided below.
class ViewController: UIViewController {
#IBOutlet weak var container: UIView!
let imageView = UIImageView()
var initial: CGFloat = .zero
var dropped = false
private func excedesLimit() -> Bool {
// < set here desired bounce limits
return imageView.frame.minX < -180 || imageView.frame.minX > 80
}
#IBAction func pressHandler(_ sender: UILongPressGestureRecognizer) {
let location = sender.location(in: imageView.superview).x
if sender.state == .began {
dropped = false
initial = location - imageView.center.x
}
else if !dropped {
if (sender.state == .changed) {
imageView.center = CGPoint(x: location - initial, y: imageView.center.y)
dropped = excedesLimit()
}
if sender.state == .ended || dropped {
initial = .zero
// variant with animator
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
let stickTo: CGFloat = self.imageView.frame.minX < -100 ? -100 : 0 // place for button at right
self.imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: self.imageView.frame.origin.y), size: self.imageView.frame.size)
}
animator.isInterruptible = true
animator.startAnimation()
// uncomment below - variant with UIViewAnimation
// UIView.beginAnimations("bounce", context: nil)
// UIView.setAnimationDuration(0.2)
// UIView.setAnimationTransition(.none, for: imageView, cache: true)
// UIView.setAnimationBeginsFromCurrentState(true)
//
// let stickTo: CGFloat = imageView.frame.minX < -100 ? -100 : 0 // place for button at right
// imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: imageView.frame.origin.y), size: imageView.frame.size)
// UIView.setAnimationDelegate(self)
// UIView.setAnimationDidStop(#selector(makeBounce))
// UIView.commitAnimations()
}
}
}
// #objc func makeBounce() {
// let bounceAnimation = CABasicAnimation(keyPath: "position.x")
// bounceAnimation.duration = 0.1
// bounceAnimation.repeatCount = 0
// bounceAnimation.autoreverses = true
// bounceAnimation.fillMode = kCAFillModeBackwards
// bounceAnimation.isRemovedOnCompletion = true
// bounceAnimation.isAdditive = false
// bounceAnimation.timingFunction = CAMediaTimingFunction(name: "easeOut")
// imageView.layer.add(bounceAnimation, forKey:"bounceAnimation");
// }
override func viewDidLoad() {
super.viewDidLoad()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(named: "cat")
imageView.contentMode = .scaleAspectFill
imageView.layer.borderColor = UIColor.red.cgColor
imageView.layer.borderWidth = 1.0
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
container.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true
imageView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 1).isActive = true
imageView.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 1).isActive = true
let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(pressHandler(_:)))
pressGesture.minimumPressDuration = 0
pressGesture.allowableMovement = .infinity
imageView.addGestureRecognizer(pressGesture)
}
}

How to properly transform UIView's scale on UIScrollView movement

To have a similar effect to Snapchat's HUD movement, I have created a movement of the HUD elements based on UIScollView's contentOffset. Edit: Link to the Github project.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.view.layoutIfNeeded()
let factor = scrollView.contentOffset.y / self.view.frame.height
self.transformElements(self.playButton,
0.45 + 0.55 * factor, // 0.45 = desired scale + 0.55 = 1.0 == original scale
Roots.screenSize.height - 280, // 280 == original Y
Roots.screenSize.height - 84, // 84 == minimum desired Y
factor)
}
func transformElements(_ element: UIView?,
_ scale: CGFloat,
_ originY: CGFloat,
_ desiredY: CGFloat,
_ factor: CGFloat) {
if let e = element {
e.transform = CGAffineTransform(scaleX: scale, y: scale) // this line lagging
let resultY = desiredY + (originY - desiredY) * factor
var frame = e.frame
frame.origin.y = resultY
e.frame = frame
}
}
With this code implemented the scroll as well as the transition appeared to be "laggy"/not smooth. (Physical iPhone 6S+ and 7+).
Deleting the following line: e.transform = CGAffineTransform(scaleX: scale, y: scale) erased the issue. The scroll as well as the Y-movement of the UIView object is smooth again.
What's the best approach to transform the scale of an object?
There are no Layout Constraints.
func setupPlayButton() {
let rect = CGRect(x: Roots.screenSize.width / 2 - 60,
y: Roots.screenSize.height - 280,
width: 120,
height: 120)
self.playButton = UIButton(frame: rect)
self.playButton.setImage(UIImage(named: "playBtn")?.withRenderingMode(.alwaysTemplate), for: .normal)
self.playButton.tintColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
self.view.addSubview(playButton)
}
This is happening because you are applying both: transform and frame. It will be smoother, if you apply only transform. Update your transformElements function as below:
func transformElements(_ element: UIView?,
_ scale: CGFloat,
_ originY: CGFloat,
_ desiredY: CGFloat,
_ factor: CGFloat) {
if let e = element {
e.transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: 0, y: desiredY * (1 - factor))
}
}
You can make these kinds of animation smoother by creating an animation then setting the speed of the layer to 0 and then changing the timeOffset of the layer.
first add the animation in the setupPlayButton method
let animation = CABasicAnimation.init(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 0.45
animation.duration = 1.0
//Set the speed of the layer to 0 so it doesn't animate until we tell it to
self.playButton.layer.speed = 0.0;
self.playButton.layer.add(animation, forKey: "transform");
next in the scrollViewDidScroll change the timeOffset of the layer and move the center of the button.
if let btn = self.playButton{
var factor:CGFloat = 1.0
if isVertically {
factor = scrollView.contentOffset.y / self.view.frame.height
} else {
factor = scrollView.contentOffset.x / Roots.screenSize.width
var transformedFractionalPage: CGFloat = 0
if factor > 1 {
transformedFractionalPage = 2 - factor
} else {
transformedFractionalPage = factor
}
factor = transformedFractionalPage;
}
//This will change the size
let timeOffset = CFTimeInterval(1-factor)
btn.layer.timeOffset = timeOffset
//now change the positions. only use center - not frame - so you don't mess up the animation. These numbers aren't right I don't know why
let desiredY = Roots.screenSize.height - (280-60);
let originY = Roots.screenSize.height - (84-60);
let resultY = desiredY + (originY - desiredY) * (1-factor)
btn.center = CGPoint.init(x: btn.center.x, y: resultY);
}
I couldn't quite get the position of the button correct - so something is wrong with my math there, but I trust you can fix it.
If you want more info about this technique see here: http://ronnqvi.st/controlling-animation-timing/

SKShapeNode coordinates seem reversed when using UIPanGestureRecognizer

I've run into a problem with swift when creating a SKShapeNode where it seems as though my 'y' coordinate is inverted.
Initially I place my node at the bottom of the screen and added a physics body (which is placed correctly given that I've turned set skView.showsPhysics to true and verified this) but whenever i try to interact with the node with UIPanGestureRecognizer it will not move unless I interact with the very upper, opposite side of the screen. I've found that it will react no matter where the node is, but i must invert the Y location of my input.
Furthermore if i drag downward on the inverted location, the node will move downward, so in some sense it is not completely inverted in that it will still interact in the correct direction.
At this point I believe it has something to do with the different coordinate systems but I've been unable to isolate which part of my code is causing the problem. I've included my code below.
override func didMoveToView(view: SKView){
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePan:"))
self.view?.addGestureRecognizer(gestureRecognizer)
physicsWorld.gravity = CGVector(dx: 0.0, dy: -2.0)
physicsWorld.contactDelegate = self
let userShape = SKShapeNode(rectOfSize: CGSize(width: 30, height: 30))
userShape.position = CGPointMake(self.frame.size.width/2, userShape.frame.size.height/2)
userShape.name = "userShape"
userShape.fillColor = SKColor.blueColor()
userShape.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 30, height: 30))
userShape.physicsBody?.dynamic = true
userShape.physicsBody?.affectedByGravity = false
userShape.physicsBody?.mass = 0.02
userShape.physicsBody?.friction = 0.0
userShape.physicsBody?.restitution = 0.5
addChild(userShape)
}
my handler
func handlePan(recognizer: UIPanGestureRecognizer!)
{
switch recognizer.state{
case .Began:
var touchLocation = recognizer.locationInView(recognizer.view!)
selectNodeForTouch(touchLocation)
case .Changed:
var translation = recognizer.translationInView(recognizer.view!)
translation = CGPointMake(translation.x, translation.y)
self.panForTranslation(translation)
recognizer.setTranslation(CGPointZero, inView: recognizer.view!)
case .Ended:
var velocity = recognizer.velocityInView(recognizer.view)
_selectedNode.physicsBody!.applyImpulse(CGVectorMake(velocity.x * velocityController, velocity.y * velocityController * -1.0))
default:
break;
}
}
func panForTranslation(translation: CGPoint)
{
var position = _selectedNode.position
print(_selectedNode.position)
_selectedNode.position = CGPointMake(position.x + translation.x, position.y - translation.y)
}
func selectNodeForTouch(touchLocation: CGPoint)//NODE SELECTOR
{
var touchedNode = nodeAtPoint(touchLocation)
_selectedNode = touchedNode
}
I've figured out that it does have to do with some sort of conversation of coordinate systems. In handlePan within the .begin statement i placed this line directly after creating the touchLocation variable
"touchLocation = self.convertPointFromView(touchLocation)" and it seems to have fixed the problem.