How to make animations and gesture recognisers work together? (Swift) - ios

I made a simple project, with swipe gesture recogniser and animation. I made my label to move and every 3 second increase number. With every swipe I need to decrease the number. My gesture recogniser object is tied with label, i.e. it works only in label bounds. When prog is working without animation everything is ok, but when it;s animated an is moving my gesture recogniser is doing nothing. How to make a gesture recogniser work at the same time as animation, i.e. while animated to respond to my swipes. Need help.
`
#IBOutlet weak var label1: UILabel!
var number : Int = 0
var timer = Timer()
#IBAction func label1SwipeRight(_ sender: UISwipeGestureRecognizer) {
number += 1
label1.text = String(number)
}
func animate1() {
UIView.animate(withDuration: 4.0, delay: 0.0, options: .allowUserInteraction, animations: {
let num1 : CGFloat = CGFloat(arc4random_uniform(667))
let num2 : CGFloat = CGFloat(arc4random_uniform(375))
self.label1.frame.origin.y = num1
self.label1.frame.origin.x = num2
}, completion: {(bool) in
self.animate1()
print("Animation1 completed")
})
}
func timerExample() {
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
label1.text = String(Int(label1.text!)! + 1)
}`

By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:
UIView.animate(duration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})
Note, however, that if what you're animating is a view's position, the user won't be able to tap on the view object as it moves. That's because a position animation does not really animate the object from one place to another over time. It just creates that appearance. Behind the scenes, the object actually jumps to it's final position the moment the animation begins.
If you need to be able to tap/drag/swipe on objects while they're moving you will have to do that yourself. What you do is put a gesture recognizer on the parent view that encloses the entire range of motion (possibly the whole screen.) Then you need to use the presentation layer of your animating view's layer, translate the coordinates of the point from the gesture recognizer's coordinate space to the layer's coordinate space, and use the layer's hitTest method to figure out if the point is on the layer or not.
I have a project on Github called iOS-CAAnimation-group-demo that does something like that (It animates an image view along a complex path and you can tap on the image view to pause the animation while it's "in flight".
It's from several years ago, so it's written in Objective-C, but it should help to at least illustrate the technique.

Related

constraints are reapplied when timer is running and changes label text

I'm working on a swift camera app and trying to solve the following problem.
My camera app, which allows taking a video, can change the camera focus and exposure when a user taps somewhere on the screen. Like the default iPhone camera app, I displayed the yellow square to show where the user tapped. I can see exactly where the user tapped unless triggering the timer function and updating the total video length on the screen.
Here is the image of the camera app.
As you can see there is a yellow square and that was the point I tapped on the screen.
However, when the timer counts up (in this case, 19 sec to 20sec) and updates the text of total video length, the yellow square moves back to the center of the screen. Since I put the yellow square at the center of the screen on my storyboard, I guess when the timer counts up and updates the label text, it also refreshing the yellow square, UIView, so displaying at the center of the screen (probably?).
So if I'm correct, how can I display the yellow square at the user tapped location regardless of the timer function, which updates UIlabel for every second?
Here is the code.
final class ViewController: UIViewController {
private var pointOfInterestHalfCompletedWorkItem: DispatchWorkItem?
#IBOutlet weak var pointOfInterestView: UIView!
#IBOutlet weak var time: UILabel!
var counter = 0
var timer = Timer()
override func viewDidLayoutSubviews() {
pointOfInterestView.layer.borderWidth = 1
pointOfInterestView.layer.borderColor = UIColor.systemYellow.cgColor
}
#objc func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
#objc func timerAction() {
counter += 1
secondsToHoursMinutesSeconds(seconds: counter)
}
func secondsToHoursMinutesSeconds (seconds: Int) {
// format seconds
time.text = "\(strHour):\(strMin):\(strSec)"
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let focusPoint = touches.first!.location(in: lfView)
showPointOfInterestViewAtPoint(point: focusPoint)
}
func showPointOfInterestViewAtPoint(point: CGPoint) {
print("point is here \(point)")
pointOfInterestHalfCompletedWorkItem = nil
pointOfInterestComplatedWorkItem = nil
pointOfInterestView.center = point
pointOfInterestView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
let animation = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.pointOfInterestView.transform = .identity
self.pointOfInterestView.alpha = 1
}
animation.startAnimation()
let pointOfInterestHalfCompletedWorkItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
let animation = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.pointOfInterestView.alpha = 0.5
}
animation.startAnimation()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: pointOfInterestHalfCompletedWorkItem)
self.pointOfInterestHalfCompletedWorkItem = pointOfInterestHalfCompletedWorkItem
}
}
Since I thought it's a threading issue, I tried to change the label text & to show the yellow square in the main thread by writing DispatchQueue.main.asyncAfter, but it didn't work. Also, I was not sure if it becomes serial queue or concurrent queue if I perform both
loop the timer function and constantly updating label text
detect UI touch event and show yellow square
Since UI updates are performed in the main thread, I guess I need to figure out a way to share the main thread for the timer function and user touch event...
If someone knows a clue to solve this problem, please let me know.
It isn't a threading issue. It is an auto layout issue.
Presumably you have positioned the yellow square view in your storyboard using constraints.
You are then modifying the yellow square's frame directly by modifying the center property; this has no effect on the constraints that are applied to the view. As soon as the next auto layout pass runs (triggered by the text changing, for example) the constraints are reapplied and the yellow square jumps back to where your constraints say it should be.
You have a couple of options;
Compute the destination point offset from the center of the view and then apply those offsets to the constant property of your two centering constraints
Add the yellow view programatically and position it by setting its frame directly. You can then adjust the frame by modifying center as you do now.

UIButton can't handle touch events while it's animating [duplicate]

I made a simple project, with swipe gesture recogniser and animation. I made my label to move and every 3 second increase number. With every swipe I need to decrease the number. My gesture recogniser object is tied with label, i.e. it works only in label bounds. When prog is working without animation everything is ok, but when it;s animated an is moving my gesture recogniser is doing nothing. How to make a gesture recogniser work at the same time as animation, i.e. while animated to respond to my swipes. Need help.
`
#IBOutlet weak var label1: UILabel!
var number : Int = 0
var timer = Timer()
#IBAction func label1SwipeRight(_ sender: UISwipeGestureRecognizer) {
number += 1
label1.text = String(number)
}
func animate1() {
UIView.animate(withDuration: 4.0, delay: 0.0, options: .allowUserInteraction, animations: {
let num1 : CGFloat = CGFloat(arc4random_uniform(667))
let num2 : CGFloat = CGFloat(arc4random_uniform(375))
self.label1.frame.origin.y = num1
self.label1.frame.origin.x = num2
}, completion: {(bool) in
self.animate1()
print("Animation1 completed")
})
}
func timerExample() {
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
label1.text = String(Int(label1.text!)! + 1)
}`
By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:
UIView.animate(duration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})
Note, however, that if what you're animating is a view's position, the user won't be able to tap on the view object as it moves. That's because a position animation does not really animate the object from one place to another over time. It just creates that appearance. Behind the scenes, the object actually jumps to it's final position the moment the animation begins.
If you need to be able to tap/drag/swipe on objects while they're moving you will have to do that yourself. What you do is put a gesture recognizer on the parent view that encloses the entire range of motion (possibly the whole screen.) Then you need to use the presentation layer of your animating view's layer, translate the coordinates of the point from the gesture recognizer's coordinate space to the layer's coordinate space, and use the layer's hitTest method to figure out if the point is on the layer or not.
I have a project on Github called iOS-CAAnimation-group-demo that does something like that (It animates an image view along a complex path and you can tap on the image view to pause the animation while it's "in flight".
It's from several years ago, so it's written in Objective-C, but it should help to at least illustrate the technique.

Repeating animation on tap

I want to rotate a UIImage, I have managed to do so with the code below, however when I press the rotate button again, the image does not rotate anymore, could someone please explain why?
#IBAction func rotate(sender: UIButton) {
UIView.animateWithDuration(0.2, animations: {
self.shape.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4) * 2)
})
}
You are changing your shape image view's transform to a new, fixed value. If you tap on it again, the transform already has that value. You set the transform to the same value again, which doesn't change anything.
You need to define an instance variable to keep track of the rotation.
var rotation: CGFloat = 0
#IBAction func rotate(sender: UIButton)
{
UIView.animateWithDuration(0.2, animations:
{
self.rotation += CGFloat(M_PI_4) * 2 //changed based on Daniel Storm's comment
self.shape.transform = CGAffineTransformMakeRotation(rotation)
})
}
That way, each time your tap the button you'll change the rotation variable from it's previous value to a new value and rotate to that new angle.

Why does the Swipe Gesture Recognizer only work once?

I set up a Swipe Gesture Recognizer and I connected it to the code, so that an UIImageView rotates when the user swipes to the left.
#IBAction func swipeToLeft(sender: AnyObject) {
UIView.animateWithDuration(1.0, animations: {
self.image.transform = CGAffineTransformRotate(self.image.transform, -3.14159265358979 )
})
}
I made sure the viewDidLoad method looked like this:
image.userInteractionEnabled = true
However, the UIImageView only gets transformed only once.
You can download the demo of the project from this link. Why is that happening?
I guess, the problem is that the moment you rotated your image, the gesture recognizer, associated with it, also got rotated. You can make sure yourself:
Make a swipe right-to-left. The image will rotate. Then make a swipe left-to-right. It will rotate again.
If you want to always handle a swipe right-to-left, you can do it in a couple of ways:
If your view is always rotated by 180 degrees, the easiest way is to change the gesture recognizer orientation (#LyndseyScott was faster than me to right the code for this one, you can check her answer :) ).
Another option (especially, if there might be a situation, when you rotate your view by some arbitrary angle), is to create a UIView on top of the view you want to rotate (but not as its subview!), and to add the gesture recognizer to it instead.
Just to elaborate on FreeNickname's answer, since the gesture recognizer "rotates" along with the UIImageView, you can change your code to the following so that the swipe gesture swaps directions along with the image and you can continue swiping from right to left to trigger the animation:
#IBAction func swipeToLeft(sender: UISwipeGestureRecognizer) {
UIView.animateWithDuration(1.0, animations: {
self.doubleDot.transform = CGAffineTransformRotate(self.doubleDot.transform, -3.14159265358979 )
}, completion: {
(value: Bool) in
if sender.direction == UISwipeGestureRecognizerDirection.Left {
sender.direction = UISwipeGestureRecognizerDirection.Right
} else {
sender.direction = UISwipeGestureRecognizerDirection.Left
}
})
}

Swift: I'm super confused! UIGravityBehavior vs NSTimer vs CGPointMake on UILabel

So I declared a UILabel! named "textLabel" which has a gravity effect which goes up. Now, When it reachs -30, I want the label to reappear at (200, 444). I used a NSTimer to check if (labelText > -30) but when it reaches/passes that point, it just flashes where it's suppose to appear (around the middle) but it does not start from the middle. here is most of my code.
How do I make the Label appear at it's new position? so it can just cycle again once it reaches that -30 on the y-axis?? I've searched and searched. HELPPPPPPP
#IBOutlet weak var textLabel: UILabel!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var itemBehavior: UIDynamicItemBehavior!
var boundaryTimer = NSTimer()
Override func viewDidLoad() {
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [textLabel])
animator.addBehavior(gravity)
boundaryTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "leftBoundary", userInfo: nil, repeats: true)
itemBehavior = UIDynamicItemBehavior(items: [textLabel])
itemBehavior.elasticity = 1.2
gravity.gravityDirection = CGVectorMake(0.0 , -0.01)
animator.addBehavior(itemBehavior)
}
func leftBoundary() {
if textLabel.center.y < 40 {
self.textLabel.center = CGPointMake(200, 444)
}
}
Rather than setting the center, you can add and then remove an attachment behavior, e.g.:
func leftBoundary() {
if textLabel.center.y < 40 {
let attachment = UIAttachmentBehavior(item: textLabel, attachedToAnchor: textLabel.center)
animator.addBehavior(attachment)
attachment.anchorPoint = view.center
attachment.action = {[unowned self, attachment] in
if self.textLabel.center.y > 100 {
self.animator.removeBehavior(attachment)
}
}
}
}
Note, you might want to change the itemBehavior so that allowRotation is false (depending upon your desired UI).
Also note, this action that I use here is something you could use with your gravity behavior, too. So, rather than a timer (which might not always catch the label at the same time), use an action, which is called upon every frame of the animation.
You should first remove all label behaviors from the animator.
This has to do with how UIKit Dynamics works. It really only manipulates the layer / presentation layer of your view, so you need to reset everything if you want to set the position yourself (via the view, which also affects the layer).
Also, I think the setup with the timer is not very efficient. Rather, you should give KVO (key-value observing) a try. You can observe the key path "center.y" and react if it passes your limit.

Resources