Completion block of animation is performed immediately - ios

I'm trying to remove the custom view from the superview after the end of the animation in the completion block, but it is called immediately and the animation becomes sharp. I managed to solve the problem in a not very good way: just adding a delay to remove the view.
Here is the function for animating the view:
private func animatedHideSoundView(toRight: Bool) {
let translationX = toRight ? 0.0 : -screenWidth
UIView.animate(withDuration: 0.5) {
self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
} completion: { isFinished in
if isFinished {
self.soundView.removeFromSuperview()
self.songPlayer.pause()
}
}
}
The problem in this line: self.soundView.removeFromSuperview()
When I call this function in the switch recognizer.state completion block statement it executes early and when elsewhere everything works correctly.
#objc private func soundViewPanned(recognizer: UIPanGestureRecognizer) {
let touchPoint = recognizer.location(in: view)
switch recognizer.state {
case .began:
initialOffset = CGPoint(x: touchPoint.x - soundView.center.x, y: touchPoint.y - soundView.center.y)
case .changed:
soundView.center = CGPoint(x: touchPoint.x - initialOffset.x, y: touchPoint.y - initialOffset.y)
if notHiddenSoundViewRect.minX > soundView.frame.minX {
animatedHideSoundView(toRight: false)
} else if notHiddenSoundViewRect.maxX < soundView.frame.maxX {
animatedHideSoundView(toRight: true)
}
case .ended, .cancelled:
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let velocity = recognizer.velocity(in: view)
let projectedPosition = CGPoint(
x: soundView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
y: soundView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
let nearestCornerPosition = nearestCorner(to: projectedPosition)
let relativeInitialVelocity = CGVector(
dx: relativeVelocity(forVelocity: velocity.x, from: soundView.center.x, to: nearestCornerPosition.x),
dy: relativeVelocity(forVelocity: velocity.y, from: soundView.center.y, to: nearestCornerPosition.y)
)
let timingParameters = UISpringTimingParameters(dampingRatio: 0.8, initialVelocity: relativeInitialVelocity)
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: timingParameters)
animator.addAnimations {
self.soundView.center = nearestCornerPosition
}
animator.startAnimation()
default: break
}
}
I want the user to be able to swipe this soundView off the screen.
That's why I check where the soundView is while the user is moving it, so that if he moves the soundView near the edge of the screen, I can hide the soundView animatedly.
Maybe I'm doing it wrong, but I couldn't think of anything else, because I don't have much experience. Could someone give me some advice on this?
I managed to solve it this way, but I don't like it:
private func animatedHideSoundView(toRight: Bool) {
let translationX = toRight ? 0.0 : -screenWidth
UIView.animate(withDuration: 0.5) {
self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.soundView.removeFromSuperview()
self.songPlayer.pause()
}
}
enter image description here
You can see and run all code here: https://github.com/swiloper/AnimationProblem

Couple notes...
First, in your controller code, you are calling animatedHideSoundView() from your pan gesture recognizer every time you move the touch. It's unlikely that's what you want to do.
Second, if you call animatedHideSoundView(toRight: true) your code:
private func animatedHideSoundView(toRight: Bool) {
let translationX = toRight ? 0.0 : -screenWidth
UIView.animate(withDuration: 0.5) {
self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
} completion: { isFinished in
if isFinished {
self.soundView.removeFromSuperview()
self.songPlayer.pause()
}
}
}
sets translationX to Zero ... when you then try to animate the transform, the animation will take no time because you're not changing the x.
Third, I strongly suggest that you start simple. The code you linked to cannot be copy/pasted/run, which makes it difficult to offer help.
Here's a minimal version of your UniversalTypesViewController class (it uses your linked SoundView class):
final class UniversalTypesViewController: UIViewController {
// MARK: Properties
private lazy var soundView = SoundView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
private let panGestureRecognizer = UIPanGestureRecognizer()
private var initialOffset: CGPoint = .zero
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
panGestureRecognizer.addTarget(self, action: #selector(soundViewPanned(recognizer:)))
soundView.addGestureRecognizer(panGestureRecognizer)
}
private func animatedShowSoundView() {
// reset soundView's transform
soundView.transform = .identity
// add it to the view
view.addSubview(soundView)
// position soundView near bottom, but past the right side of view
soundView.frame.origin = CGPoint(x: view.frame.width, y: view.frame.height - soundView.frame.height * 2.0)
soundView.startSoundBarsAnimation()
// animate soundView into view
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut) {
self.soundView.transform = CGAffineTransform(translationX: -self.soundView.frame.width * 2.0, y: 0.0)
}
}
private func animatedHideSoundView(toRight: Bool) {
let translationX = toRight ? view.frame.width : -(view.frame.width + soundView.frame.width)
UIView.animate(withDuration: 0.5) {
self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
} completion: { isFinished in
if isFinished {
self.soundView.removeFromSuperview()
//self.songPlayer.pause()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// if soundView is not in the view hierarchy,
// animate it into view - animatedShowSoundView() func adds it as a subview
if soundView.superview == nil {
animatedShowSoundView()
} else {
// unwrap the touch
guard let touch = touches.first else { return }
// get touch location
let loc = touch.location(in: self.view)
// if touch is inside the soundView frame,
// return, so pan gesture can move soundView
if soundView.frame.contains(loc) { return }
// if touch is on the left-half of the screen,
// animate soundView to the left and remove after animation
if loc.x < view.frame.midX {
animatedHideSoundView(toRight: false)
} else {
// touch is on the right-half of the screen,
// so just remove soundView
animatedHideSoundView(toRight: true)
}
}
}
// MARK: Objc methods
#objc private func soundViewPanned(recognizer: UIPanGestureRecognizer) {
let touchPoint = recognizer.location(in: view)
switch recognizer.state {
case .began:
initialOffset = CGPoint(x: touchPoint.x - soundView.center.x, y: touchPoint.y - soundView.center.y)
case .changed:
soundView.center = CGPoint(x: touchPoint.x - initialOffset.x, y: touchPoint.y - initialOffset.y)
case .ended, .cancelled:
()
default: break
}
}
}
If you run that, tapping anywhere will animate soundView into view at bottom-right. You can then drag soundView around.
If you tap away from soundView frame, on the left-half of the screen, soundView will be animated out to the left and removed after animation completes.
If you tap away from soundView frame, on the right-half of the screen, soundView will be animated out to the right and removed after animation completes.
Once you've got that working, and you see what's happening, you can implement it in the rest of your much-more-complex code.
Edit
Take a look at this modified version of your code.
One big problem in your code is that you're making multiple calls to animatedHideSoundView(). When the drag gets near the edge, your code calls that... but then it gets called again because the drag is still "active."
So, I added a var isHideAnimationRunning: Bool flag so calls to positioning when dragging and positioning when "hide" animating don't conflict.
A few other changes:
instead of mixing Transforms with .center positioning, get rid of the Transforms and just use .center
I created a struct with logically named corner points - makes it much easier to reference them
strongly recommended: add comments to your code!
So, give this a try:
import UIKit
let screenWidth: CGFloat = UIScreen.main.bounds.width
let screenHeight: CGFloat = UIScreen.main.bounds.height
let sideSpacing: CGFloat = 32.0
let mediumSpacing: CGFloat = 16.0
var isNewIphone: Bool {
return screenHeight / screenWidth > 1.8
}
extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
return sqrt(pow(point.x - x, 2) + pow(point.y - y, 2))
}
}
// so we can refer to corner positions by logical names
struct CornerPoints {
var topLeft: CGPoint = .zero
var bottomLeft: CGPoint = .zero
var bottomRight: CGPoint = .zero
var topRight: CGPoint = .zero
}
final class ViewController: UIViewController {
private var cornerPoints = CornerPoints()
private let soundViewSide: CGFloat = 80.0
private lazy var halfSoundViewWidth = soundViewSide / 2
private lazy var newIphoneSpacing = isNewIphone ? mediumSpacing : 0.0
private lazy var soundView = SoundView(frame: CGRect(origin: .zero, size: CGSize(width: soundViewSide, height: soundViewSide)))
private lazy var notHiddenSoundViewRect = CGRect(x: mediumSpacing, y: 0.0, width: screenWidth - mediumSpacing * 2, height: screenHeight)
private var initialOffset: CGPoint = .zero
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// setup corner points
let left = sideSpacing + halfSoundViewWidth
let right = view.frame.maxX - (sideSpacing + halfSoundViewWidth)
let top = sideSpacing + halfSoundViewWidth - newIphoneSpacing
let bottom = view.frame.maxY - (sideSpacing + halfSoundViewWidth - newIphoneSpacing)
cornerPoints.topLeft = CGPoint(x: left, y: top)
cornerPoints.bottomLeft = CGPoint(x: left, y: bottom)
cornerPoints.bottomRight = CGPoint(x: right, y: bottom)
cornerPoints.topRight = CGPoint(x: right, y: top)
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.addTarget(self, action: #selector(soundViewPanned(recognizer:)))
soundView.addGestureRecognizer(panGestureRecognizer)
// for development, let's add a double-tap recognizer to
// add the soundView again (if it's been removed)
let dt = UITapGestureRecognizer(target: self, action: #selector(showAgain(_:)))
dt.numberOfTapsRequired = 2
view.addGestureRecognizer(dt)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.animatedShowSoundView()
}
}
#objc func showAgain(_ f: UITapGestureRecognizer) {
// if soundView has been removed
if soundView.superview == nil {
// add it
animatedShowSoundView()
}
}
private func animatedShowSoundView() {
// start at bottom-right, off-screen to the right
let pt: CGPoint = cornerPoints.bottomRight
soundView.center = CGPoint(x: screenWidth + soundViewSide, y: pt.y)
view.addSubview(soundView)
soundView.startSoundBarsAnimation()
// animate to bottom-right corner
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut) {
self.soundView.center = pt
}
}
// flag so we know if soundView is currently
// "hide" animating
var isHideAnimationRunning: Bool = false
private func animatedHideSoundView(toRight: Bool) {
// only execute if soundView is not currently "hide" animating
if !isHideAnimationRunning {
// set flag to true
isHideAnimationRunning = true
// target center X
let targetX: CGFloat = toRight ? screenWidth + soundViewSide : -soundViewSide
UIView.animate(withDuration: 0.5) {
self.soundView.center.x = targetX
} completion: { isFinished in
self.isHideAnimationRunning = false
if isFinished {
self.soundView.removeFromSuperview()
//self.songPlayer.pause()
}
}
}
}
#objc private func soundViewPanned(recognizer: UIPanGestureRecognizer) {
let touchPoint = recognizer.location(in: view)
switch recognizer.state {
case .began:
// only execute if soundView is not currently "hide" animating
if !isHideAnimationRunning {
initialOffset = CGPoint(x: touchPoint.x - soundView.center.x, y: touchPoint.y - soundView.center.y)
}
case .changed:
// only execute if soundView is not currently "hide" animating
if !isHideAnimationRunning {
soundView.center = CGPoint(x: touchPoint.x - initialOffset.x, y: touchPoint.y - initialOffset.y)
if notHiddenSoundViewRect.minX > soundView.frame.minX {
animatedHideSoundView(toRight: false)
} else if notHiddenSoundViewRect.maxX < soundView.frame.maxX {
animatedHideSoundView(toRight: true)
}
}
case .ended, .cancelled:
// only execute if soundView is not currently "hide" animating
if !isHideAnimationRunning {
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let velocity = recognizer.velocity(in: view)
let projectedPosition = CGPoint(
x: soundView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
y: soundView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
let nearestCornerPosition = nearestCorner(to: projectedPosition)
let relativeInitialVelocity = CGVector(
dx: relativeVelocity(forVelocity: velocity.x, from: soundView.center.x, to: nearestCornerPosition.x),
dy: relativeVelocity(forVelocity: velocity.y, from: soundView.center.y, to: nearestCornerPosition.y)
)
let timingParameters = UISpringTimingParameters(dampingRatio: 0.8, initialVelocity: relativeInitialVelocity)
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: timingParameters)
animator.addAnimations {
self.soundView.center = nearestCornerPosition
}
animator.startAnimation()
}
default: break
}
}
private func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
}
private func nearestCorner(to point: CGPoint) -> CGPoint {
var minDistance = CGFloat.greatestFiniteMagnitude
var nearestPosition = CGPoint.zero
for position in [cornerPoints.topLeft, cornerPoints.bottomLeft, cornerPoints.bottomRight, cornerPoints.topRight] {
let distance = point.distance(to: position)
if distance < minDistance {
nearestPosition = position
minDistance = distance
}
}
return nearestPosition
}
/// Calculates the relative velocity needed for the initial velocity of the animation.
private func relativeVelocity(forVelocity velocity: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat {
guard currentValue - targetValue != 0 else { return 0 }
return velocity / (targetValue - currentValue)
}
}

Related

How to combine translate and rotate transform animation in Swift(iOS)?

I created a small UIView object, First I use translate transform move UIView to new location, after animation stop, I want UIView rotate in new location, so the code is:
//circle2 is UIView
UIView.animate(withDuration: 1, delay:0,options:[.curveLinear],animations:{
self.circle2.transform=CGAffineTransform(translationX: 0, y: 35)
},completion: {(result) in
UIView.animate(withDuration: 1, delay:0,options:[.repeat],animations: {
self.circle2.transform=CGAffineTransform(rotationAngle: CGFloat.pi)
//self.circle2.transform=CGAffineTransform(rotationAngle: CGFloat.pi).translatedBy(x: 0, y: 35)
//self.circle2.transform=CGAffineTransform(translationX: 0, y: 35).rotated(by: CGFloat.pi)
//self.circle2.transform=CGAffineTransform(rotationAngle: CGFloat.pi).concatenating(CGAffineTransform(translationX: 0, y: 35))
})
})
But I found that when UIView is rotating, it also move upward to origin position.
I try three another combination method, none of then works....
Two of your examples do work but animation may be different than what you expected. You will need to do it manually. It is painful but please try and examine the following code:
private func startAnimation()
let startTime: Date = .init()
let translationDuration: TimeInterval = 1.0
var rotation: CGFloat = 0.0
let rotationDuration: TimeInterval = 1.0
var translation: CGPoint = .zero
func refreshView(_ controller: ViewController) {
controller.circle2.transform = CGAffineTransform(rotationAngle: rotation).concatenating(CGAffineTransform(translationX: translation.x, y: translation.y))
}
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let now: Date = .init()
let scale: Double = (now.timeIntervalSince(startTime))/translationDuration
guard scale > 0.0 else { return } // Waiting part if delay is applied
if scale < 1.0 {
// Animate the view
translation = CGPoint(x: 0.0, y: 35.0*scale)
refreshView(self)
} else {
// Animation ended
timer.invalidate()
translation = CGPoint(x: 0.0, y: 35.0)
refreshView(self)
var rotationStartTime: Date = .init()
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
let now: Date = .init()
let scale: Double = (now.timeIntervalSince(rotationStartTime))/rotationDuration
guard scale > 0.0 else { return } // Waiting part if delay is applied
if scale < 1.0 {
// Animate the view
rotation = .pi * CGFloat(scale)
refreshView(self)
} else {
// Animation ended
rotation = 0.0
rotationStartTime = .init()
refreshView(self)
}
}
}
}
}
If you have a need to stop the animation then you will need some references to timers.

Animation bug when swiping UIView

I'm developing an app which has a 3 view and which is a card view like in Tinder. I'm creating views in a for loop. When I have more than 4 views, everything works fine. When It has only 3 cards, everything looks okey at first ,when the app opens, but after swiping one card, It gets broken. Last card moves with some bug. I'm trying to edit the code to work with 3 card but can't figure out. By the way, ImageCard is just a UIView class.
EDIT: My problem is that when It has 3 cards, App opens with 3 cards shown on screen but after a swipe, last card doesn't show on the screen, only 2 cards shown in screen. After swipe card on the front should goes to backmost and 3 cards should be seen again. When It has more than 5 cards, everything works fine like I explained and 3 cards shown on screen (What It needs to be)
I'm sure showNextCard() function occurs the problem but to be sure here is the full code :
class WelcomeViewController: UIViewController {
/// Data structure for custom cards
var cards = [ImageCard]()
override func viewDidLoad() {
super.viewDidLoad()
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
print(self.view.frame.height)
print(self.view.frame.width)
let screenWidth = self.view.frame.width
let screenHeight = self.view.frame.height
//When add new cards to self.cards and call layoutCards() again
for i in 1...5 {
let card = ImageCard(frame: CGRect(x: 0, y: 0, width: screenWidth - screenWidth / 5, height: screenWidth))
card.tag = i
card.label.text = "Card Number: \(i)"
cards.append(card)
}
lastIndex = cards.count
// 2. layout the first cards for the user
layoutCards()
}
/// Scale and alpha of successive cards visible to the user
let cardAttributes: [(downscale: CGFloat, alpha: CGFloat)] = [(1, 1), (0.92, 0.8), (0.84, 0.6), (0.76, 0.4)]
let cardInteritemSpacing: CGFloat = 12
/// Set up the frames, alphas, and transforms of the first 4 cards on the screen
func layoutCards() {
// frontmost card (first card of the deck)
let firstCard = cards[0]
self.view.addSubview(firstCard)
firstCard.layer.zPosition = CGFloat(cards.count)
firstCard.center = self.view.center
firstCard.frame.origin.y += 23
firstCard.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleCardPan)))
// the next 3 cards in the deck
for i in 1...3 {
if i > (cards.count - 1) { continue }
let card = cards[i]
card.layer.zPosition = CGFloat(cards.count - i)
// here we're just getting some hand-picked vales from cardAttributes (an array of tuples)
// which will tell us the attributes of each card in the 4 cards visible to the user
let downscale = cardAttributes[i].downscale
let alpha = cardAttributes[i].alpha
card.transform = CGAffineTransform(scaleX: downscale, y: downscale)
card.alpha = alpha
// position each card so there's a set space (cardInteritemSpacing) between each card, to give it a fanned out look
card.center.y = self.view.center.y + 23
card.frame.origin.x = cards[0].frame.origin.x + (CGFloat(i) * cardInteritemSpacing * 3)
// workaround: scale causes heights to skew so compensate for it with some tweaking
if i == 3 {
card.frame.origin.x += 1.5
}
self.view.addSubview(card)
}
// make sure that the first card in the deck is at the front
self.view.bringSubview(toFront: cards[0])
}
/// This is called whenever the front card is swiped off the screen or is animating away from its initial position.
/// showNextCard() just adds the next card to the 4 visible cards and animates each card to move forward.
func showNextCard() {
let animationDuration: TimeInterval = 0.2
// 1. animate each card to move forward one by one
for i in 1...3{
if i > (cards.count - 1) { continue }
let card = cards[i]
let newDownscale = cardAttributes[i - 1].downscale
let newAlpha = cardAttributes[i - 1].alpha
UIView.animate(withDuration: animationDuration, delay: (TimeInterval(i - 1) * (animationDuration / 2)), usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [], animations: {
card.transform = CGAffineTransform(scaleX: newDownscale, y: newDownscale)
card.alpha = newAlpha
if i == 1 {
card.center = self.view.center
card.frame.origin.y += 23
} else {
card.center.y = self.view.center.y + 23
card.frame.origin.x = self.cards[1].frame.origin.x + (CGFloat(i - 1) * self.cardInteritemSpacing * 3)
}
}, completion: { (_) in
if i == 1 {
card.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleCardPan)))
}
})
}
// 2. add a new card (now the 4th card in the deck) to the very back
if 4 > (cards.count - 1) {
if cards.count != 1 {
self.view.bringSubview(toFront: cards[1])
}else{
//self.view.bringSubview(toFront: cards.last!)
}
return
}
let newCard = cards[4]
newCard.layer.zPosition = CGFloat(cards.count - 4)
let downscale = cardAttributes[3].downscale
let alpha = cardAttributes[3].alpha
// initial state of new card
newCard.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
newCard.alpha = 0
newCard.center.y = self.view.center.y + 23
newCard.frame.origin.x = cards[1].frame.origin.x + (4 * cardInteritemSpacing * 3)
self.view.addSubview(newCard)
// animate to end state of new card
UIView.animate(withDuration: animationDuration, delay: (3 * (animationDuration / 2)), usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [], animations: {
newCard.transform = CGAffineTransform(scaleX: downscale, y: downscale)
newCard.alpha = alpha
newCard.center.y = self.view.center.y + 23
newCard.frame.origin.x = self.cards[1].frame.origin.x + (3 * self.cardInteritemSpacing) + 1.5
}, completion: { (_) in
})
// first card needs to be in the front for proper interactivity
self.view.bringSubview(toFront: self.cards[1])
}
/// Whenever the front card is off the screen, this method is called in order to remove the card from our data structure and from the view.
func removeOldFrontCard() {
cards.append(cards[0])
cards[0].removeFromSuperview()
cards.remove(at: 0)
layoutCards()
}
private func isVerticalGesture(_ recognizer: UIPanGestureRecognizer) -> Bool {
let translation = recognizer.translation(in: self.view!)
if fabs(translation.y) > fabs(translation.x) {
return true
}
return false
}
/// UIKit dynamics variables that we need references to.
var dynamicAnimator: UIDynamicAnimator!
var cardAttachmentBehavior: UIAttachmentBehavior!
/// This method handles the swiping gesture on each card and shows the appropriate emoji based on the card's center.
#objc func handleCardPan(sender: UIPanGestureRecognizer) {
// Ensure it's a horizontal drag
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
// if we're in the process of hiding a card, don't let the user interace with the cards yet
if cardIsHiding { return }
// change this to your discretion - it represents how far the user must pan up or down to change the option
// distance user must pan right or left to trigger an option
let requiredOffsetFromCenter: CGFloat = 80
let panLocationInView = sender.location(in: view)
let panLocationInCard = sender.location(in: cards[0])
switch sender.state {
case .began:
dynamicAnimator.removeAllBehaviors()
let offset = UIOffsetMake(cards[0].bounds.midX, panLocationInCard.y)
// card is attached to center
cardAttachmentBehavior = UIAttachmentBehavior(item: cards[0], offsetFromCenter: offset, attachedToAnchor: panLocationInView)
//dynamicAnimator.addBehavior(cardAttachmentBehavior)
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
if(sender.view!.center.x < 555) {
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
}else {
sender.view!.center = CGPoint(x:sender.view!.center.x, y:554)
}
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .changed:
//cardAttachmentBehavior.anchorPoint = panLocationInView
let translation = sender.translation(in: self.view)
print(sender.view!.center.y)
if(sender.view!.center.x < 555) {
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
}else {
sender.view!.center = CGPoint(x:sender.view!.center.x, y:554)
}
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .ended:
dynamicAnimator.removeAllBehaviors()
if !(cards[0].center.x > (self.view.center.x + requiredOffsetFromCenter) || cards[0].center.x < (self.view.center.x - requiredOffsetFromCenter)) {
// snap to center
let snapBehavior = UISnapBehavior(item: cards[0], snapTo: CGPoint(x: self.view.frame.midX, y: self.view.frame.midY + 23))
dynamicAnimator.addBehavior(snapBehavior)
} else {
let velocity = sender.velocity(in: self.view)
let pushBehavior = UIPushBehavior(items: [cards[0]], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x/10, dy: velocity.y/10)
pushBehavior.magnitude = 175
dynamicAnimator.addBehavior(pushBehavior)
// spin after throwing
var angular = CGFloat.pi / 2 // angular velocity of spin
let currentAngle: Double = atan2(Double(cards[0].transform.b), Double(cards[0].transform.a))
if currentAngle > 0 {
angular = angular * 1
} else {
angular = angular * -1
}
let itemBehavior = UIDynamicItemBehavior(items: [cards[0]])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angular), for: cards[0])
dynamicAnimator.addBehavior(itemBehavior)
showNextCard()
hideFrontCard()
}
default:
break
}
}
/// This function continuously checks to see if the card's center is on the screen anymore. If it finds that the card's center is not on screen, then it triggers removeOldFrontCard() which removes the front card from the data structure and from the view.
var cardIsHiding = false
func hideFrontCard() {
if #available(iOS 10.0, *) {
var cardRemoveTimer: Timer? = nil
cardRemoveTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] (_) in
guard self != nil else { return }
if !(self!.view.bounds.contains(self!.cards[0].center)) {
cardRemoveTimer!.invalidate()
self?.cardIsHiding = true
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn], animations: {
self?.cards[0].alpha = 0.0
}, completion: { (_) in
self?.removeOldFrontCard()
self?.cardIsHiding = false
})
}
})
} else {
// fallback for earlier versions
UIView.animate(withDuration: 0.2, delay: 1.5, options: [.curveEaseIn], animations: {
self.cards[0].alpha = 0.0
}, completion: { (_) in
self.removeOldFrontCard()
})
}
}
}
ImageCard Class:
class ImageCard: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
// card style
self.backgroundColor = UIColor.blue
self.layer.cornerRadius = 26
label.font = Font.gothamBold?.withSize(30)
label.textColor = UIColor.white
self.addSubview(label)
label.anchor(self.topAnchor, left: self.leftAnchor, bottom: nil, right: nil, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I found you forget to turn off your dynamicAnimator after animations. At least, you need to turn off animator about cards[0]. Otherwise, it becomes unpredictable. You can use your removeOldFrontCard() like this. Hope this is the answer.
func removeOldFrontCard() {
dynamicAnimator.removeAllBehaviors()
cards.append( cards.remove(at: 0))
layoutCards()
}
You start at index 1 but index of an Array starts with 0
// the next 3 cards in the deck
for i in 1...3 {
if i > (cards.count - 1) { continue }
let card = cards[i]
...
}
Change that to:
// the next 3 cards in the deck
for i in 0...2 {
if i > (cards.count - 1) { break }
let card = cards[i]
...
}

Using UIPanGestureRecognizer to swipe down UIView

I have an IBAction for a UIPanGestureRecognizer in my UIView, I am able to handle the gesture and recognise the state changes from began, cancelled and ended, as well as respond to those changes.
However when using the sender.location to handle the swipe down, the UIView actually moves up once the gesture has began, and then continues to move down. The experience is jarring and I am not sure what I am doing wrong. Does anybody have any ideas ?
func update(_ translation: CGPoint, origin: CGPoint) {
let offSetY = translation.y
cardView.frame.origin.y = offSetY
let multiplier = 1 - (translation.y / 2000)
self.view.alpha = multiplier
}
func cancel(_ origin: CGPoint) {
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 0.6) {
self.visualEffectView.alpha = 1
self.cardView.alpha = 1.0
self.view.alpha = 1.0
self.cardView.center = origin
}
animator.startAnimation()
}
func finish() {
let animator = UIViewPropertyAnimator(duration: 0.9, dampingRatio: 0.9) {
self.visualEffectView.effect = nil
self.dismiss(animated: true, completion: nil)
}
animator.startAnimation()
}
#IBAction func panGestureAction(_ sender: UIPanGestureRecognizer) {
self.view.backgroundColor = .white
let originalCardPosition = cardView.center
//let cardOriginY = cardView.frame.origin.y
let translation = sender.translation(in: self.cardView)
let origin = sender.location(in: self.cardView)
switch sender.state {
case .changed:
if translation.y > 0 { update(translation, origin: origin) }
case .ended:
let translation = sender.translation(in: self.cardView)
if translation.y > 100 {
finish()
} else {
cardView.alpha = 1.0
cancel(originalCardPosition)
}
case .cancelled:
cancel(originalCardPosition)
default: break
}
}
The problem is that you set the origin.y of the cardView directly to the value of translation.y. You need to add translation.y to the view's original y value determined when the gesture begins.
Add a property to the class:
var originalY: CGFloat = 0
Then in the .began state of the gesture, set it:
originalY = cardView.frame.origin.y
Then in your update method you set the origin:
cardView.frame.origin.y = originalY + offSetY

Rotating ImageView using UIPanGestureRecognizer- Swift 3

I am trying to rotate an ImageView I have depending on the X coordinate it is on. Basically, I want it to have a rotation of 0º when x = 300 and a rotation of 180º when x = 190.
I had to program the UIPanGestureRecognizer programmatically. Here is the code I currently have right now:
#objc func personDrag(recognizer: UIPanGestureRecognizer) {
let rotationSub: CGFloat = 1
let translation = recognizer.translation(in: rView)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x, y:view.center.y + translation.y)
view.transform = view.transform.rotated(by: CGFloat.pi - rotationSub)
}
recognizer.setTranslation(CGPoint.zero, in: rView)
}
I was going to attempt to change the rotation degree by 1 every time they panned but it doesn't really work/make sense. Any help would be appreciated. Thank you so much!
Cheers, Theo
You can build your implementation on this:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageview: UIImageView!
private var currentRotation: Rotation = .none
/* Certain rotation points (rotation of 0º when x = 300 and a rotation of 180º when x = 190) */
enum Rotation {
case none, xPoint190, xPoint300
}
override func viewDidLoad() {
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
imageview.addGestureRecognizer(gestureRecognizer)
imageview.isUserInteractionEnabled = true
}
#IBAction func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard gestureRecognizer.state == .began || gestureRecognizer.state == .changed else {
return
}
guard let imgView = gestureRecognizer.view else {
return
}
let translation = gestureRecognizer.translation(in: self.view)
imgView.center = CGPoint(x: imgView.center.x + translation.x, y: imgView.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
let angle: CGFloat = self.degreesToRadians(180.0)
/* After reaching x point case - rotating and setting rotation occured to prohibit further rotation */
if imgView.layer.frame.origin.x <= 190, currentRotation != .xPoint190 {
imgView.transform = imgView.transform.rotated(by: angle)
currentRotation = .xPoint190
} else if imgView.layer.frame.origin.x >= 300, currentRotation != .xPoint300 {
imgView.transform = imgView.transform.rotated(by: angle)
currentRotation = .xPoint300
}
private func degreesToRadians(_ deg: CGFloat) -> CGFloat {
return deg * CGFloat.pi / 180
}
}
I hope this will help you.
#objc func rotateViewPanGesture(_ recognizer: UIPanGestureRecognizer) {
touchLocation = recognizer.location(in: superview)
let center = CGRectGetCenter(frame)
switch recognizer.state {
case .began:
deltaAngle = atan2(touchLocation!.y - center.y, touchLocation!.x - center.x) - CGAffineTrasformGetAngle(transform)
initialBounds = bounds
initialDistance = CGpointGetDistance(center, point2: touchLocation!)
case .changed:
let ang = atan2(touchLocation!.y - center.y, touchLocation!.x - center.x)
let angleDiff = deltaAngle! - ang
let a = transform.a
let b = transform.b
let c = transform.c
let d = transform.d
let sx = sqrt(a * a + b * b)
let sy = sqrt(c * c + d * d)
let currentScale = CGPoint(x: sx, y: sy)
let scale = CGAffineTransform(scaleX: currentScale.x, y: currentScale.y)
self.transform = scale.rotated(by: -angleDiff)
layoutIfNeeded()
case .ended:
print("end gesture status")
default:break
}
}
Using Swift5
Programmatically
Rotate view by single point touch
import UIKit
class ViewController: UIViewController {
//Variable for rotating
private var deltaAngle:CGFloat = 0
let squareView : UIView = {
let anyView = UIView()
anyView.backgroundColor = .red
anyView.isUserInteractionEnabled = true
anyView.isMultipleTouchEnabled = true
return anyView
}()
let rotateButton : UIButton = {
let button = UIButton()
button.backgroundColor = .black
button.setImage(UIImage(systemName: "rotate.right"), for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
squareView.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
rotateButton.frame = CGRect(x: 0, y: squareView.frame.height-30, width: 30, height: 30)
squareView.center = view.center
view.addSubview(squareView)
squareView.addSubview(rotateButton)
let PanToRotate = UIPanGestureRecognizer(target: self, action: #selector(handleRotateGesture(_:)))
rotateButton.addGestureRecognizer(PanToRotate)
}
#objc func handleRotateGesture(_ recognizer : UIPanGestureRecognizer){
let touchLocation = recognizer.location(in: squareView.superview)
let center = squareView.center
switch recognizer.state{
case .began :
self.deltaAngle = atan2(touchLocation.y - center.y, touchLocation.x - center.x) - atan2(squareView.transform.b, squareView.transform.a)
case .changed:
let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
let angleDiff = self.deltaAngle - angle
squareView.transform = CGAffineTransform(rotationAngle: -angleDiff)
default: break
}
}
}

Subview to TabBarController is blocking tab bar items in iOS

I'm working on something like the draggable video player in youtube app. I've been able to make the swipe down work and I'm adding the video player view as a subview to the Tab bar controller which is the root view of my application. The issue is when I swipe down to minimise the video, I'm not able to select two tab bar items underneath the video player. How do I solve this?
In the image, the Live and Me tabs are not selectable unless I remove the video player from the screen.
My view hierarchy is UITabBarController > UINavigationController > UIViewController so i subclassed UINavigationController:
class CFNavigationController: UINavigationController, CFVideoPlayerControllerDelegate {
//MARK: Properties
lazy var videoPlayerViewController: CFVideoPlayerController = {
let pvc: CFVideoPlayerController = self.storyboard?.instantiateViewController(withIdentifier: "videoPlayer") as! CFVideoPlayerController
pvc.view.frame = CGRect.init(origin: self.hiddenOrigin, size: UIScreen.main.bounds.size)
pvc.delegate = self
return pvc
}()
let statusView: UIView = {
let st = UIView.init(frame: UIApplication.shared.statusBarFrame)
st.backgroundColor = UIColor.white
st.alpha = 0.15 // 0.15
return st
}()
let hiddenOrigin: CGPoint = {
let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 10
let x = -UIScreen.main.bounds.width
let coordinate = CGPoint.init(x: x, y: y)
return coordinate
}()
let minimizedOrigin: CGPoint = {
let x = UIScreen.main.bounds.width/2 - 10
let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 60
let coordinate = CGPoint.init(x: x, y: y)
return coordinate
}()
let fullScreenOrigin = CGPoint.init(x: 0, y: 0)
//Methods
func animatePlayView(toState: stateOfVC) {
switch toState {
case .fullScreen:
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: UIViewAnimationOptions.beginFromCurrentState, animations: {
self.videoPlayerViewController.view.frame.origin = self.fullScreenOrigin
})
case .minimized:
UIView.animate(withDuration: 0.3, animations: {
self.videoPlayerViewController.view.frame.origin = self.minimizedOrigin
})
case .hidden:
UIView.animate(withDuration: 0.3, animations: {
self.videoPlayerViewController.view.frame.origin = self.hiddenOrigin
self.videoPlayerViewController.videoPlayerView.player?.replaceCurrentItem(with: nil)
})
}
}
func positionDuringSwipe(scaleFactor: CGFloat) -> CGPoint {
let width = UIScreen.main.bounds.width * 0.5 * scaleFactor
let height = width * 9 / 16
let x = (UIScreen.main.bounds.width - 10) * scaleFactor - width
let y = (UIScreen.main.bounds.height - 10 ) * scaleFactor - height
let coordinate = CGPoint.init(x: x, y: y)
return coordinate
}
//MARK: Delegate methods
func didMinimize() {
self.animatePlayView(toState: .minimized)
}
func didMaximize(){
self.animatePlayView(toState: .fullScreen)
}
func didEndedSwipe(toState: stateOfVC){
self.animatePlayView(toState: toState)
}
func swipeToMinimize(translation: CGFloat, toState: stateOfVC){
switch toState {
case .fullScreen:
self.videoPlayerViewController.view.frame.origin = self.positionDuringSwipe(scaleFactor: translation)
case .hidden:
// self.videoPlayerViewController.view.frame.origin.x = UIScreen.main.bounds.width/2 - abs(translation) - 10
self.videoPlayerViewController.removeFromParentViewController()
case .minimized:
self.videoPlayerViewController.view.frame.origin = self.positionDuringSwipe(scaleFactor: translation)
}
}
//MARK: ViewController lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.view.addSubview(self.videoPlayerViewController.view)
self.tabBarController?.view.bringSubview(toFront: self.videoPlayerViewController.view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
// Give View Controllers the power of their own rotation
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return visibleViewController?.supportedInterfaceOrientations ?? super.supportedInterfaceOrientations
}
open override var shouldAutorotate: Bool {
return visibleViewController?.shouldAutorotate ?? super.shouldAutorotate
} }
Here is how I add it to as subview:
self.tabBarController?.view.addSubview(self.videoPlayerViewController.view)
self.tabBarController?.view.bringSubview(toFront: self.videoPlayerViewController.view)

Resources