Setup UIPanGestureRecognizer for both direction - swift

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.

Related

Use pan gestures to rotate view in one corner swift

I'm trying to add a swiping future to a view, but I want it to move by moving the finger tip on it, around a corner of the view and when it reach a certain point, it stop moving.
I did it with UISwipeGestureRecognizerbut it moves automatically with animation, also the it's not exactly rotating around corner
let swipeToRight = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeRight))
swipeToRight.direction = .right
container.addGestureRecognizer(swipeToRight)
#objc func respondToSwipeRight(gesture: UIGestureRecognizer) {
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let this = self else { return }
this.backgroundView.backgroundColor = UIColor.systemGreen
this.container.setAnchorPoint(anchorPoint: .init(x: 1, y: 1))
this.container.transform = CGAffineTransform(rotationAngle: 15.degreesToRadians)
haptic.impactOccurred()
}, completion: { [weak self] _ in
guard let this = self else { return }
this.rotateContainerToInitialPosition()
})
}
I want to do it with pan gestures or something similar that it let the view to move exactly with finger, and come back when it's released , but I don't know how to do it. could anyone show me how? Thank you so much
Here's an example of using a UIPanGestureRecognizer to rotate the box 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.
class ViewController: UIViewController {
var box: UIView!
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]
box.anchorPoint = CGPoint(x: 1, y: 1) // bottom-right corner for rotation
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)
// Ignore moves to the left of the starting point
let right = max(offset.x, 0)
// Only rotate up to 75 degrees - just an example
let angleDeg = min(right / box.bounds.size.width * 90, 75)
// Create and set the rotation transform on the green box
let rotate = CGAffineTransform(rotationAngle: angleDeg / 180 * .pi)
box.transform = rotate
}
}
}
Start a pan in the box - move right and left and the green box rotates back and forth as the finger moves. Start a new pan and the rotation resets.

Aligning four CAEmitterLayer() on each side of view, pointing inward

I'm working on adding 4 CAEmitterLayers to a subview. Each CAEmitterLayer is a line, and will be positioned on each side. Rotation will be controlled via CGAffineTransform
My problem:
I can't get the .Left and .Right emitterPosition to properly line up with the left and right side of the view
Here's my progress:
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = GlobalConstants.smallCornerRadius
createParticles(direction: .Up)
createParticles(direction: .Down)
createParticles(direction: .Left)
createParticles(direction: .Right)
}
enum EmitTo {
case Up
case Down
case Left
case Right
}
func createParticles(direction: EmitTo) {
self.layoutSubviews()
let particleEmitter = CAEmitterLayer()
particleEmitter.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
particleEmitter.bounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
if direction == .Up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: 0))
} else if (direction == .Down) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi))
} else if (direction == .Left) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi / 2))
} else if (direction == .Right) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi / 2))
}
if direction == .Up || direction == .Down {
particleEmitter.emitterPosition = CGPoint(x: self.frame.width / 2, y: 0)
}
if direction == .Left {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: self.frame.height)
}
if direction == .Right {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: -50)
}
particleEmitter.emitterSize = self.bounds.size
particleEmitter.emitterShape = CAEmitterLayerEmitterShape.line
particleEmitter.zPosition = 1
let triangle = makeEmitterCell(direction: direction)
particleEmitter.emitterCells = [triangle]
self.layer.addSublayer(particleEmitter)
}
func makeEmitterCell(direction: EmitTo) -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
What's happening:
My question
How can I get the .Left and .Right CAEmitterLayer to properly align with the left side and right side of the gray view?
Green Triangle Image:
Your approach to try to use CGAffineTransform is promising because a line emitter is a horizontal line that has only a width dimension. By rotating it, you can have it emit from the left or right.
However you need to use emitter bounds with the view height as width and the view width as height.
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
Accordingly, one sets the width of the emitterSize to the height of the view:
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
The emitter position must be adjusted in the same way.
Why is this so? Take a look at the following illustration with an emission from the top:
If you rotate the layer by 90 degrees you would only cover a small area in the middle. Also the emissions would not be visible, because they would be to much outside to the left.
The difference between having the emitter on the left or the right is just a call to setAffineTransform with either -.pi / 2 or .pi / 2.
The emitters from top and bottom of course should get the normal bounds, they only need to be rotated as you do in your question.
Hints
In your createParticles method you call self.layoutSubviews(). That should be avoided.
Apple docs explicitly state:
You should not call this method directly.
see https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews.
Instead you could override layoutSubviews and call your createParticles methods from there. Make sure to remove any previously created emitter layers by removing them.
One small thing: Swift switch cases start with a lowercase letter by convention.
Code
A complete example to help you get started might then look something like this:
import UIKit
class EmitterView: UIView {
private var emitters = [CAEmitterLayer]()
enum EmitTo {
case up
case down
case left
case right
}
override func layoutSubviews() {
super.layoutSubviews()
emitters.forEach { $0.removeFromSuperlayer() }
emitters.removeAll()
createParticles(direction: .up)
createParticles(direction: .down)
createParticles(direction: .left)
createParticles(direction: .right)
}
func createParticles(direction: EmitTo) {
let particleEmitter = CAEmitterLayer()
emitters.append(particleEmitter)
particleEmitter.position = CGPoint(x: bounds.midX, y: bounds.midY)
particleEmitter.emitterShape = .line
switch direction {
case .up, .down:
particleEmitter.bounds = self.bounds
particleEmitter.emitterPosition = CGPoint(x: bounds.size.width / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.width, height: 0)
if direction == .up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi))
}
case .left, .right:
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
particleEmitter.emitterPosition = CGPoint(x: bounds.height / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
let rotationAngle: CGFloat = direction == EmitTo.left ? -.pi / 2 : .pi / 2
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: rotationAngle))
}
particleEmitter.emitterCells = [makeEmitterCell()]
layer.addSublayer( particleEmitter)
}
func makeEmitterCell() -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
}
Test

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)
}
}

Scale UITextView with pan gesture

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
}
}

how to replace panGestureDidMove with fixed start to end position

I have been using UIPanGestureRecognizer to recognise touches but I want to replace it with a fixed start to end position for my animation. Please see the code below:
panGestureDidMove:
func panGestureDidMove(gesture: UIPanGestureRecognizer) {
if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled {
} else {
let additionalHeight = max(gesture.translationInView(view).y, 0)
let waveHeight = min(additionalHeight * 0.6, maxWaveHeight)
let baseHeight = minimalHeight + additionalHeight - waveHeight
let locationX = gesture.locationInView(gesture.view).x
layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX)
updateShapeLayer()
}
}
layoutControlPoints:
private func layoutControlPoints(baseHeight baseHeight: CGFloat, waveHeight: CGFloat, locationX: CGFloat) {
let width = view.bounds.width
let minLeftX = min((locationX - width / 2.0) * 0.28, 0.0)
let maxRightX = max(width + (locationX - width / 2.0) * 0.28, width)
let leftPartWidth = locationX - minLeftX
let rightPartWidth = maxRightX - locationX
l3ControlPointView.center = CGPoint(x: minLeftX, y: baseHeight)
l2ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.44, y: baseHeight)
l1ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
cControlPointView.center = CGPoint(x: locationX , y: baseHeight + waveHeight * 1.36)
r1ControlPointView.center = CGPoint(x: maxRightX - rightPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
r2ControlPointView.center = CGPoint(x: maxRightX - (rightPartWidth * 0.44), y: baseHeight)
r3ControlPointView.center = CGPoint(x: maxRightX, y: baseHeight)
}
I am trying to replace the panGestureDidMove with CABasicAnimation to animate the start to end position, something like the code below:
let startValue = CGPointMake(70.0, 50.0)
let endValue = CGPointMake(90.0, 150.0)
CATransaction.setDisableActions(true) //Not necessary
view.layer.bounds.size.height = endValue
let positionAnimation = CABasicAnimation(keyPath:"bounds.size.height")
positionAnimation.fromValue = startValue
positionAnimation.toValue = endValue
positionAnimation.duration = 2.0
view.layer.addAnimation(positionAnimation, forKey: "bounds")
A lot of things are affected as the position changes, how can I achieve this?
If you want fine control of animation, you can use CADisplayLink to do any custom layout or drawing prior to the screens pixel refresh. The run loop attempts to draw 60 frames per second, so with that in mind you can modify your code to simulate touch events.
You'll need to add some properties:
var displayLink:CADisplayLink? // let's us tap into the drawing run loop
var startTime:NSDate? // while let us keep track of how long we've been animating
var deltaX:CGFloat = 0.0 // how much we should update in the x direction between frames
var deltaY:CGFloat = 0.0 // same but in the y direction
var startValue = CGPointMake(70.0, 50.0) // where we want our touch simulation to start
var currentPoint = CGPoint(x:0.0, y:0.0)
let endValue = CGPointMake(90.0, 150.0) // where we want our touch simulation to end
Then whenever we want the animation to run we can call:
func animate()
{
let duration:CGFloat = 2.0
self.currentPoint = self.startValue
self.deltaX = (endValue.x - startValue.x) / (duration * 60.0) // 60 frames per second so getting the difference then dividing by the duration in seconds times 60
self.deltaY = (endValue.y - startValue.y) / (duration * 60.0)
self.startTime = NSDate()
self.displayLink = CADisplayLink(target: self, selector: #selector(self.performAnimation))
self.displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
}
This will set up the display link and determine my how much our touch simulation should move between frames and begin calling the function that will be called prior to every drawing of the screen performAnimation:
func performAnimation(){
if self.startTime?.timeIntervalSinceNow > -2 {
self.updateViewsFor(self.currentPoint, translation: CGPoint(x:self.currentPoint.x - self.startValue.x, y: self.currentPoint.y - self.startValue.y))
self.currentPoint.x += self.deltaX
self.currentPoint.y += self.deltaY
}
else
{
self.displayLink?.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
self.currentPoint = self.startValue
}
}
Here we check if we are within the duration of the animation. Then call updateViewsFor(point:CGPoint,translation:CGPoint) which is what you had in your gesture target and then update our touch simulation if we're within it, else we just reset our properties.
Finally,
func updateViewsFor(point:CGPoint,translation:CGPoint)
{
let additionalHeight = max(translation.y, 0)
let waveHeight = min(additionalHeight * 0.6, maxWaveHeight)
let baseHeight = minimalHeight + additionalHeight - waveHeight
let locationX = point.x
layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX)
updateShapeLayer()
}
You could also change your panGestureDidMove to:
#objc func panGestureDidMove(gesture: UIPanGestureRecognizer) {
if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled {
} else {
self.updateViewsFor(gesture.locationInView(gesture.view), translation: gesture.translationInView(view))
}
}
Edit
There is an easier way of doing this using keyframe animation. But I'm not sure it will animate your updateShapeLayer(). But for animating views we can write a function like:
func animate(fromPoint:CGPoint, toPoint:CGPoint, duration:NSTimeInterval)
{
// Essentually simulates the beginning of a touch event at our start point and the touch hasn't moved so tranlation is zero
self.updateViewsFor(fromPoint, translation: CGPointZero)
// Create our keyframe animation using UIView animation blocks
UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: .CalculationModeLinear, animations: {
// We really only have one keyframe which is the end. We want everything in between animated.
// So start time zero and relativeDuration 1.0 because we want it to be 100% of the animation
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1.0, animations: {
// we want our "touch" to move to the endValue and the translation will just be the difference between end point and start point in the x and y direction.
self.updateViewsFor(toPoint, translation: CGPoint(x: toPoint.x - fromPoint.x, y: toPoint.y - fromPoint.y))
})
}, completion: { _ in
// do anything you need done after the animation
})
}
This will move the views into place then create a keyframe for where the views end up and animate everything in between. We could call it like:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.animate(CGPointMake(70.0, 50.0), toPoint: CGPointMake(90.0, 150.0), duration: 2.0)
}