When I tap my scrollView it activates a UITapGestureRecognizer that animates the contentOffset for about 2 seconds. How could I allow the user to interrupt the animation and take full control of the scrollView again when they drag? Right now the user has to wait until the end of the animation to start interacting with the scrollView again.
Note: self refers to the scrollView
Tap set up:
let singleTap = UITapGestureRecognizer(target: self, action: "subtleBounce:")
singleTap.cancelsTouchesInView = false
self.addGestureRecognizer(singleTap)
So when the user taps:
func subtleBounce(gesture : UITapGestureRecognizer) {
let originalFrame = self.frame
UIView.animateWithDuration(0.1, delay: 0.0, usingSpringWithDamping: 1.5, initialSpringVelocity: 1.5, options: UIViewAnimationOptions.CurveLinear, animations: {
self.contentOffset.y -= 10.0
}, completion: {
Void in
UIView.animateWithDuration(0.6, delay: 0.0, usingSpringWithDamping: 0.1, initialSpringVelocity: 3.0, options: UIViewAnimationOptions.CurveLinear, animations: {
self.contentOffset.y += 10.0 }, completion: { Void in
})
})
}
So the code above works as intended.
Here is what I've tried to stop the animation and give the user control of the scrollView again:
let drag = UISwipeGestureRecognizer(target: self, action: "stopScrollAnimation:")
drag.cancelsTouchesInView = false
self.addGestureRecognizer(drag)
and elsewhere:
func stopScrollAnimation(gesture : UISwipeGestureRecognizer) {
self.layer.removeAllAnimations()
self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
}
However this DOES NOT work as intended. The animation still controls the scrollView and the user cannot interact with it.
If you want an example what I mean, look at the iOS7/8 lockscreen. After tapping the screen it also starts an animation. The user can take control of the scrollView mid animation however.
edit: accepting answers in swift or obj-c.
Add AllowUserInteraction to your animation options.
Related
I’ve created a draggable button over an UICollectionView, and it works well. However, whenever I pull down the UICollectionView to update data, the draggable button would always be back to the original coordinate.
On the other hand, I also want to set a timer for the draggable button title, it means the title will be updated every second. But the draggable button would be back to original coordinate as well when the title is changed.
I’ve tried to use millisecond for the timeInterval, but it wastes more CPU and battery consumption. So I’d like ask if there’s any better way to deal with it?
【Update】
I recreate another project for this, please refer to the gif as below.
I place the button at the middle of the view, and the title will be added 1 every second. So I was wondering why the button always get back to the original coordinate whenever the title is changed.
Gif Here!
【Second Update】
I set constraints for the button, and use UIPanGestureRecognizer to change the position of the button. Please refer to the following code.
private func setupFloatingBtn() {
view.addSubview(self.floatingBtn)
self.floatingBtn.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.floatingBtn.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
self.floatingBtn.widthAnchor.constraint(equalToConstant: 60).isActive = true
self.floatingBtn.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.floatingBtn.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handler)))
}
#objc func handler(gesture: UIPanGestureRecognizer) {
let location = gesture.location(in: self.view)
let draggedView = gesture.view
draggedView?.center = location
if gesture.state == .ended {
if self.floatingBtn.frame.midX >= self.view.layer.frame.width / 2 {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.floatingBtn.center.x = self.view.layer.frame.width - 40
}, completion: nil)
}else{
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.floatingBtn.center.x = 40
}, completion: nil)
}
}
}
I'm trying to make pull up menu view
I made this pull up view using xib and attached to ViewController
and added pan gesture on pull up view
lastly, I updated height constraint using pan gesture for animation.
func pullUpControlView(_ pullUpControlView: PullUpControlView, didPanned height: CGFloat, animated: Bool) {
self.pullUpControlViewHeightConstraint?.constant = height
if animated {
UIView.animate(withDuration: 10, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
this is my code it's animation works fine when view got maximum height,
but when minimising view, view is minimised immediately and start move to down slowly (weird animation)
how can I make this animation correctly (shrink height from top to bottom)
func pullUpControlView(_ pullUpControlView: PullUpControlView, didPanned height: CGFloat, animated: Bool) {
UIView.animate(withDuration: 5, animations: {() -> Void in
self.view.layoutIfNeeded()
}, completion: {(_ finished: Bool) -> Void in
self.pullUpControlViewHeightConstraint?.constant = height
self.view.layoutIfNeeded()
//if animation is finished ("finished" == *YES*), then hidden = "finished" ... (aka hidden = *YES*)
})
}
I see in some apps when you come to a screen with a tableview there's a short animation of the cell starting to be swiped, showing the red "swipe to delete" button (UIContextualAction button) and then it returns to normal. It is giving the user the hint: "These rows can be swiped."
Is there a way to achieve this effect? Maybe a way to programmatically start a row swipe then cancel it?
Swift Solution
Well, about 1.5 years later I finally came up with a solution.
Step 1 - Cell UI Setup
I set up my custom table view cell like this:
A and B represent the swipe action colors.
C is the UIView that I will animate side-to-side.
Step 2 - Add Animation to Cell
func animateSwipeHint() {
slideInFromRight()
}
private func slideInFromRight() {
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.curveEaseOut], animations: {
self.cellBackgroundView.transform = CGAffineTransform(translationX: -self.swipeHintDistance, y: 0)
self.cellBackgroundView.layer.cornerRadius = 10
}) { (success) in
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveLinear], animations: {
self.cellBackgroundView.transform = .identity
}, completion: { (success) in
// Slide from left if you have leading swipe actions
self.slideInFromLeft()
})
}
}
private func slideInFromLeft() {
UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseOut], animations: {
self.cellBackgroundView.transform = CGAffineTransform(translationX: self.swipeHintDistance, y: 0)
}) { (success) in
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveLinear], animations: {
self.cellBackgroundView.transform = .identity
})
}
}
Step 3 - Trigger the Animation
In the viewDidLoad of the view controller that has the table view, I have this code:
if self.tableView.visibleCells.count > 0 {
let cell = self.tableView.visibleCells[0] as! TableViewCell
cell.animateSwipeHint()
}
Example:
Video Solution
I created a video if you'd like a more in-depth walkthrough of this solution:
https://youtu.be/oAGoFd_GrxE
I have a piece of code that I saw long time ago to animate a view. Since our UITableViewCell is also a view, we can use it :) You just need to get your visible cell to animate, like so:
if let visibleCell = self.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? CustomCell {
print("Started animation...")
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 0.6
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
visibleCell.layer.add(animation, forKey: "shake")
}
Let me know if this helps. Tested it.
EDIT:
Animating your UITableView to let the user see that they can swipe on a cell is pretty easy, try it like so:
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.tableView.setEditing(true, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.tableView.setEditing(false, animated: true)
}
}
HOWEVER, if you want to swipe programmatically your cell to show your custom row actions, (I've been researching this for an hour), you can only achieve this, as far as I know, by using method swizzling. See this SO answer: http://codejaxy.com/q/186524/ios-swift-uitableview-how-to-present-uitableviewrowactions-from-pressing-a-button
To animate a bar opening...
#IBOutlet var barHeight: NSLayoutConstraint!
barHeight.constant = barShut?30:100
self.view.layoutIfNeeded()
t = !barShut?30:100
UIView.animate(withDuration: 0.15,
delay: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: { () -> Void in
self.barHeight.constant = t
self.view.layoutIfNeeded()
},
completion: {_ in
Screen.barShut = !Screen.barShut
}
)
That's great ...
But how would you make it boing like this?
(The only way I'd know to do this is, use CADisplayLink, with a few lines of code for a spring decaying.) Is this available in UIKit?
You can use the spring animation method that is built in to UIView:
func toggleBar() -> Void {
self.view.layoutIfNeeded()
let newHeight:CGFloat = !barShut ? 30:100
barShut = !barShut
barHeightConstraint.constant = newHeight
UIView.animate(withDuration: 1.5, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3, options: [], animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
You will want a longer animation duration than 0.15 of a second in order for the bounce to seem realistic; I think the values I have look pretty good, but you can play with them to get the exact effect you are after.
Since the animation duration is longer, I found that I could tap the button the triggered the open/shut while the previous animation was still running. Setting barShut in the completion block meant that the bar didn't react to all taps. I moved the toggle outside of the animation to address this.
I'm fiddling with my MKMapView in Playground, and I'm having trouble animating it's frame.size.height. Here's my Playground code:
let vc = UIViewController()
vc.view.backgroundColor = UIColor.whiteColor()
XCPlaygroundPage.currentPage.liveView = vc
let map = MKMapView(frame: vc.view.frame)
map.backgroundColor = UIColor.redColor()
map.autoresizesSubviews = false
vc.view.addSubview(map)
UIView.animateWithDuration(2.0, delay: 1.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: .CurveEaseInOut, animations: {
map.frame.size.height = 200
}, completion: nil)
You can tell when the actual animation is occurring by observing the map's red background color. The visible map's height decreases before the MKMapView's height. Setting autoresizesSubviews = false doesn't seem to do anything. So why does it seem as if there are two distinct animations?
UPDATE In my Playground, I removed all my MKMapView subviews, and the _MKMapContentView, one of the map's two subviews (the other being the "Legal" MKAttributionLabel), was removed, rendering the MKMapView with just it's red background. So the _MKMapContentView is being resized as a subview, but calling map.autoresizesSubviews = false isn't doing the trick. What gives?
This is a bit of a hack, if you will. I tried animating the map's height using animateWithDuration:delay:usingSpringWithDamping and a nonzero delay. My problem was that the _MKMapContentView wouldn't adhere to the delay, which is what explained the distinct animations.
So, with a little help from #Matt's famous delay solution, I scrapped my function delay and put the entire method in a delay block, so that any (sub)views would animate concurrently.
delay(3.5) {
UIView.animateWithDuration(1.0, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 1.0, options: .CurveEaseInOut, animations: {
map.frame.size.height -= 200
}, completion: nil)
}
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Bit of a workaround, but it works. Still eager to learn why map's subviews didn't adhere to the function delay, if anyone has an idea.
I think you maybe to user this to avoid:
map.backgroundColor = UIColor.whiteColor()
vc.view.addSubview(map)
UIView.animateWithDuration(2.0, delay: 1.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: .CurveEaseInOut, animations: {
map.frame.size.height = 200
}, completion:
map.backgroundColor = UIColor.redColor()
)