I have a view controller that setups 3 children view controllers, puts the three views side by side, and centers the middle view.. like snapchat. The sliding between view controllers is controlled by using a pangesture that adjusts the left anchor constraint of the far left child view controller.
Right now... when I pan and then let go it will go to the proper constraint.
THE ISSUE: when I pan, let go, pan again the constraints snap back and the animation isn't looking proper. Any insight would be appreciated....
this link shows what I mean by the constraint and animations snapping and being funky!! (the first pan is proper, but when i try to interrupt the second pan you see my issue)
https://giphy.com/gifs/mEDld5sbR81gJBHcl1
import UIKit
class ViewController: UIViewController {
private let viewControllerOne = UIViewController()
private let viewControllerTwo = UIViewController()
private let viewControllerThree = UIViewController()
private var viewControllerOneLeftAnchorConstraint: NSLayoutConstraint?
override func loadView() {
view = UIView()
view.backgroundColor = .white
//removed code for stackoverflow readability, here i setup view controllers and pangestures
guard let viewControllerOneLeftAnchorConstraint = viewControllerOneLeftAnchorConstraint else { return }
NSLayoutConstraint.activate([
viewControllerOne.view.topAnchor.constraint(equalTo: view.topAnchor),
viewControllerTwo.view.topAnchor.constraint(equalTo: view.topAnchor),
viewControllerThree.view.topAnchor.constraint(equalTo: view.topAnchor),
viewControllerOne.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
viewControllerTwo.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
viewControllerThree.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
viewControllerOne.view.widthAnchor.constraint(equalTo: view.widthAnchor),
viewControllerTwo.view.widthAnchor.constraint(equalTo: view.widthAnchor),
viewControllerThree.view.widthAnchor.constraint(equalTo: view.widthAnchor),
viewControllerOneLeftAnchorConstraint,
viewControllerTwo.view.leftAnchor.constraint(equalTo: viewControllerOne.view.rightAnchor),
viewControllerThree.view.leftAnchor.constraint(equalTo: viewControllerTwo.view.rightAnchor),
])
}
private var animator: UIViewPropertyAnimator?
private var previous: CGPoint?
private var current: CGPoint?
#objc private func handlePan(gesture: UIPanGestureRecognizer) {
guard let viewControllerOneLeftAnchorConstraint = viewControllerOneLeftAnchorConstraint else { return }
if gesture.state == .began {
previous = gesture.location(in: view)
animator?.stopAnimation(true)
} else if gesture.state == .changed {
let current = gesture.location(in: view)
guard let previous = previous else { return }
let xPositionDifference = current.x - previous.x
if viewControllerOneLeftAnchorConstraint.constant + xPositionDifference < 0 && viewControllerOneLeftAnchorConstraint.constant + xPositionDifference > -(UIScreen.main.bounds.width * 2) {
self.viewControllerOneLeftAnchorConstraint?.constant += xPositionDifference
self.previous = current
}
} else if gesture.state == .ended {
viewControllerOneLeftAnchorConstraint.constant = calculateLeftAnchorConstant(viewControllerOneLeftAnchorConstraint: viewControllerOneLeftAnchorConstraint)
animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeOut) {
self.view.layoutIfNeeded()
}
animator?.startAnimation()
}
}
func calculateLeftAnchorConstant(viewControllerOneLeftAnchorConstraint: NSLayoutConstraint) -> CGFloat {
if viewControllerOneLeftAnchorConstraint.constant <= 0
&& viewControllerOneLeftAnchorConstraint.constant > -(UIScreen.main.bounds.width/2.0) {
return 0.0
} else if viewControllerOneLeftAnchorConstraint.constant > -(UIScreen.main.bounds.width/2.0 * 3)
&& viewControllerOneLeftAnchorConstraint.constant < -(UIScreen.main.bounds.width/2.0) {
return -(UIScreen.main.bounds.width)
} else {
return -(UIScreen.main.bounds.width * 2)
}
}
}
I'd advise you to abandon the entirety of your code. Your words and your animated gif are describing a UIPageViewController, so why not use a UIPageViewController?
Related
I have a rotateArt function that rotates a UIImage - my problem is when this function is called following a pan gesture the image snaps back to the views centre. I would like it to rotate around its current centre.
I have tried assigning a variable to hold its current centre but that did not work. What am I missing?
Thanks
extension MainViewController {
func imageCheck() {
pickedImage != nil ? addPainting() : print("image did not load")
}
func addPainting() {
pickedImageView = UIImageView(image: pickedImage)
pickedImageView.isUserInteractionEnabled = true
pickedImageView.contentMode = .scaleAspectFit
view.addSubview(pickedImageView)
pickedImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pickedImageView.centerXAnchor.constraint(equalTo: userPickedRoom.centerXAnchor),
pickedImageView.centerYAnchor.constraint(equalTo: userPickedRoom.centerYAnchor, constant: -50),
pickedImageView.widthAnchor.constraint(equalToConstant: 150),
pickedImageView.heightAnchor.constraint(equalToConstant: 150)
])
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
pickedImageView.addGestureRecognizer(panGesture)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch))
view.addGestureRecognizer(pinchGesture)
addShadow()
}
#objc func handlePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
guard let gestureView = sender.view else {return}
gestureView.center = CGPoint(x: gestureView.center.x + translation.x, y: gestureView.center.y + translation.y)
sender.setTranslation(.zero, in: view)
}
#objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
guard sender.view != nil else { return }
if sender.state == .began || sender.state == .changed {
pickedImageView.transform = (pickedImageView.transform.scaledBy(x: sender.scale, y: sender.scale))
sender.scale = 1.0
}
}
func rotateArt() { // pickedImageView.center returns to view.center when this functinon is called.
UIImageView.animate(withDuration: 0.1, animations: {
self.pickedImageView.transform = self.pickedImageView.transform.rotated(by: .pi / 2)
})
}
func addShadow() {
pickedImageView.layer.shadowColor = UIColor.black.cgColor
pickedImageView.layer.shadowOpacity = 1
pickedImageView.layer.shadowOffset = CGSize(width: 0.1, height: 0.5)
pickedImageView.layer.shadowRadius = 1
pickedImageView.layer.shouldRasterize = true
pickedImageView.layer.rasterizationScale = UIScreen.main.scale
}
}
The problem is these lines:
pickedImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate
You cannot position pickedImageView using constraints and also animate it by changing its center (as you do during the pan gesture).
Constraints are one thing. Frames/centers are a different thing. They are opposites. You have created a conflict here: you say one thing by changing the center during the pan, but the constraints say something else (they still want to put the image view where you told them to put it).
You need to resolve that conflict. The easiest way is not to use constraints at all, but there are other ways.
From within my GameViewController's viewDidLoad() function I do two things:
1- I create a sub UIVIew, myView, which is added to the main view,
2- I call my function, addConstraints, which is in the GameViewController's code.
When I create GameScene, self.myView.frame.width & self.myView.frame.height contain the correct values, and abide by the safeAreaLayout guides.
I want to make my code clearer, so I've moved the addConstraints function to a separate file. But when I run my app that way self.myView.frame.width & self.myView.frame.height are zero.
So am curious if I am somehow translating the function incorrectly, or maybe this is something I can't move outside of the main code?
The first block is the original code, the 2nd is the function
//located with GameViewControl
private func addConstraints(){
var constraints = [NSLayoutConstraint]()
constraints.append(myView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor))
constraints.append(myView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor))
constraints.append(myView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor))
constraints.append(myView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor))
NSLayoutConstraint.activate(constraints)
}
translated into a function
//called from GameViewControl
createConstraints(view: myView)
....
//located outside of GameViewControl
func createConstraints(view: UIView) {
var constraints = [NSLayoutConstraint]()
constraints.append(view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor))
constraints.append(view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor))
constraints.append(view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor))
constraints.append(view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor))
NSLayoutConstraint.activate(constraints)
}
And here is the full file of GameViewControl
import UIKit
import SpriteKit
class GameViewController: UIViewController {
private let myView : UIView = {
let myView = UIView()
setViewAttributes(view: myView)
return myView
}()
private func addConstraints(){
var constraints = [NSLayoutConstraint]()
//add
constraints.append(myView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor))
constraints.append(myView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor))
constraints.append(myView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor))
constraints.append(myView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor))
//activate
NSLayoutConstraint.activate(constraints)
}
override func viewDidLoad() {
super.viewDidLoad()
#if DEBUG
print ("viewDidLoad")
#endif
if let view = self.view as! SKView? {
getScreenDimensions (screen: &screenDims)
view.addSubview(myView)
addConstraints()
var scene : GameScene!
DispatchQueue.main.async { scene = GameScene(size: CGSize(width: self.myView.frame.width, height: self.myView.frame.width))
scene.anchorPoint = CGPoint(x: 0.0, y: 0.0)
scene.backgroundColor = .clear
scene.scaleMode = .aspectFit
view.isHidden = false
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
view.showsPhysics = true
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
#if DEBUG
print ("GVC viewDidLayoutSubviews")
#endif
myGlobalVars.sceneRect = view.frame
if #available(iOS 11.0, *) {
myGlobalVars.topSafeArea = view.safeAreaInsets.top
myGlobalVars.bottomSafeArea = view.safeAreaInsets.bottom
} else {
myGlobalVars.topSafeArea = topLayoutGuide.length
myGlobalVars.bottomSafeArea = bottomLayoutGuide.length
}
}
override var shouldAutorotate: Bool {
#if DEBUG
print ("shouldAutorotate")
#endif
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
#if DEBUG
print ("supportedInterfaceOrientations")
#endif
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override var prefersStatusBarHidden: Bool {
#if DEBUG
print ("prefersStatusBarHidden")
#endif
return false
}
}
The call to function setViewAttributes does pass the view to a function, and I have verified that that function is working.
func setViewAttributes(view: UIView)
{
view.alpha = 0.0
view.frame.size.height = UIScreen.main.nativeBounds.height
view.frame.size.width = UIScreen.main.nativeBounds.width
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
}
You can't just move this kind of function because the view variable inside it refers to GameViewController.view.
If you want to do a refactor like this you can move this method to an extension and call it inside your GameViewController.
public extension UIView {
func createConstraints() {
guard let superview = superview else { return }
view.translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutConstraint]()
constraints.append(leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor))
constraints.append(trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor))
constraints.append(bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor))
constraints.append(topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor))
NSLayoutConstraint.activate(constraints)
}
}
And then instead of calling addConstraints() in viewDidLoad() you should call myView.createConstraints(). It should work the same as before.
You can read more about extension from Extensions — The Swift
Programming Language.
And also you should read this too Programmatically Creating
Constraints
After going through the code, over and over I realized I was calling the safeAreaLayout routine before viewDidLayoutSubviews. Now I can move my call to a function and it works. Now I'd love to know why it was working before, when I was calling it prematurely. But maybe I should be grateful that I found a substantial error.
So now my code looks the same as the code in the OP, except every call regarding screen dimensions, and safeAreaLayout, are done in the viewDidLayoutSubviews function.
And addConstraints is now an external function
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
getScreenDimensions (screen: &screenDims)
view.addSubview(myView)
addConstraints()
myGlobalVars.sceneRect = view.frame
createConstraints(view: myView)
if #available(iOS 11.0, *) {
myGlobalVars.topSafeArea = view.safeAreaInsets.top
myGlobalVars.bottomSafeArea = view.safeAreaInsets.bottom
} else {
myGlobalVars.topSafeArea = topLayoutGuide.length
myGlobalVars.bottomSafeArea = bottomLayoutGuide.length
}
}
I'm implementing custom transition using CABasicAnimation and UIView.animate both. Also need to implement a custom interactive transition using UIPercentDrivenInteractiveTransition which exactly copies the behavior of the native iOS swipe back. Animation without a back swipe gesture (when I'm pushing and popping by the back arrow) works fine and smoothly. Moreover, swipe back also works smoothly, except when the gesture velocity is more than 900
Gesture Recognition function:
#objc func handleBackGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
guard animationTransition != nil else { return }
switch gesture.state {
case .began:
interactionController = TransparentNavigationControllerTransitionInteractor(duration: anumationDuration)
popViewController(animated: true)
case .changed:
guard let view = gesture.view?.superview else { return }
let translation = gesture.translation(in: view)
var percentage = translation.x / view.bounds.size.width
percentage = min(1.0, max(0.0, percentage))
shouldCompleteTransition = percentage > 0.5
interactionController?.update(percentage)
case .cancelled, .failed, .possible:
if let interactionController = self.interactionController {
isInteractiveStarted = false
interactionController.cancel()
}
case .ended:
interactionController?.completionSpeed = 0.999
let greaterThanMaxVelocity = gesture.velocity(in: view).x > 800
let canFinish = shouldCompleteTransition || greaterThanMaxVelocity
canFinish ? interactionController?.finish() : interactionController?.cancel()
interactionController = nil
#unknown default: assertionFailure()
}
}
UIPercentDrivenInteractiveTransition class. Here I'm synchronizing layer animation.
final class TransparentNavigationControllerTransitionInteractor: UIPercentDrivenInteractiveTransition {
// MARK: - Private Properties
private var context: UIViewControllerContextTransitioning?
private var pausedTime: CFTimeInterval = 0
private let animationDuration: TimeInterval
// MARK: - Initialization
init(duration: TimeInterval) {
self.animationDuration = duration * 0.4 // I dk why but layer duration should be less
super.init()
}
// MARK: - Public Methods
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
super.startInteractiveTransition(transitionContext)
context = transitionContext
pausedTime = transitionContext.containerView.layer.convertTime(CACurrentMediaTime(), from: nil)
transitionContext.containerView.layer.speed = 0
transitionContext.containerView.layer.timeOffset = pausedTime
}
override func finish() {
restart(isFinishing: true)
super.finish()
}
override func cancel() {
restart(isFinishing: false)
super.cancel()
}
override func update(_ percentComplete: CGFloat) {
super.update(percentComplete)
guard let transitionContext = context else { return }
let progress = CGFloat(animationDuration) * percentComplete
transitionContext.containerView.layer.timeOffset = pausedTime + Double(progress)
}
// MARK: - Private Methods
private func restart(isFinishing: Bool) {
guard let transitionLayer = context?.containerView.layer else { return }
transitionLayer.beginTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
transitionLayer.speed = isFinishing ? 1 : -1
}
}
And here is my Dismissal animation function in UIViewControllerAnimatedTransitioning class
private func runDismissAnimationFrom(
_ fromView: UIView,
to toView: UIView,
in transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) else { return }
toView.frame = toView.frame.offsetBy(dx: -fromView.frame.width / 3, dy: 0)
let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
let fromViewFinalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)
// Create mask to hide bottom view with sliding
let slidingMask = CAShapeLayer()
let initialMaskPath = UIBezierPath(rect: CGRect(
x: fromView.frame.width / 3,
y: 0,
width: 0,
height: toView.frame.height)
)
let finalMaskPath = UIBezierPath(rect: toViewFinalFrame)
slidingMask.path = initialMaskPath.cgPath
toView.layer.mask = slidingMask
toView.alpha = 0
let slidingAnimation = CABasicAnimation(keyPath: "path")
slidingAnimation.fromValue = initialMaskPath.cgPath
slidingAnimation.toValue = finalMaskPath.cgPath
slidingAnimation.timingFunction = .init(name: .linear)
slidingMask.path = finalMaskPath.cgPath
slidingMask.add(slidingAnimation, forKey: slidingAnimation.keyPath)
UIView.animate(
withDuration: duration,
delay: 0,
options: animationOptions,
animations: {
fromView.frame = fromViewFinalFrame
toView.frame = toViewFinalFrame
toView.alpha = 1
},
completion: { _ in
toView.layer.mask = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
I note that glitch occurs only when a swipe has a grand velocity.
Here a video with the result of smooth animation at normal speed and not smooth at high speed - https://youtu.be/1d-kTPlhNvE
UPD:
I've already tried to use UIViewPropertyAnimator combine with
interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating
But the result is another type of glitching.
I've solved the issue, just change a part of restart function:
transitionLayer.beginTime =
transitionLayer.convertTime(CACurrentMediaTime(), from: nil) - transitionLayer.timeOffset
transitionLayer.speed = 1
I don't really understand why, but looks like timeOffset subtraction works!
How to not get detected with intersects func when moving around masked image in UIImageView frame?
explanation image
Code that I am using to detect collision:
movingPerson.frame.intersects(camera.frame)
Where movingPerson is just a regular UIImage that I am moving around with touchesmoved func and camera is a masked Image.
I've tried .bounds instead of .frame but it's not working.
Is there any easy way?
If you want the "Very easy way" then UIDynamicItem and provide a path that bounds your image and let UIDynamicAnimator handle the collisions for you. If you do not know how to get the path for your artwork (yours is pretty easy and you should be able to extract the Bezier coordinates automatically in Paintcode, by hand in Illustrator, or from a SVG file) you can use SpriteKit instead since it will actually generate the bounding polygon for your sprite automatically.
EDIT Sample (note I didn't want to write a whole app for this so I took an existing playground and just added the colliders. the collision works, but the reset after collision doesn't):
import UIKit
import PlaygroundSupport
class ObstacleView: UIView {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .path
}
override var collisionBoundingPath: UIBezierPath {
return UIBezierPath(ovalIn: bounds)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.width / 2
}
}
class ViewController: UIViewController {
var dynamicAnimator: UIDynamicAnimator!
var gravityBehavior: UIGravityBehavior!
var pushBehavior: UIPushBehavior!
var collisionBehavior: UICollisionBehavior!
lazy var player: UILabel = {
let label = UILabel()
label.text = "🐥"
label.sizeToFit()
label.isUserInteractionEnabled = true
label.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(label)
return label
}()
var isPanning = false
var anchor: CGPoint = .zero
let maxDistance = CGFloat(100)
lazy var obstacles: [UIView] = {
return (0..<2).map { index in
let view = ObstacleView(frame: CGRect(x:100 + CGFloat(index)*200,y:200, width: 40, height: 40))
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(view)
return view
}
}()
override func viewDidLoad() {
super.viewDidLoad()
dynamicAnimator = UIDynamicAnimator(referenceView: view)
dynamicAnimator.delegate = self
gravityBehavior = UIGravityBehavior(items: [player])
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
_ = obstacles
view.addGestureRecognizer(panGestureRecognizer)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
anchor = view.center
player.center = anchor
}
#objc private func pan(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
guard player.bounds.contains(recognizer.location(in: player)) else {
isPanning = false
return
}
isPanning = true
case .changed:
guard isPanning else {
return
}
player.center = recognizer.location(in: view)
let dx = player.center.x - anchor.x
let dy = player.center.y - anchor.y
let distance = CGFloat(hypotf(Float(dx), Float(dy)))
if distance > maxDistance {
player.center.x = anchor.x + dx / distance * maxDistance
player.center.y = anchor.y + dy / distance * maxDistance
}
case .ended:
guard isPanning else {
return
}
isPanning = false
let dx = player.center.x - anchor.x
let dy = player.center.y - anchor.y
let distance = CGFloat(hypotf(Float(dx), Float(dy)))
guard distance > 10 else {
player.center = anchor
return
}
if pushBehavior != nil {
dynamicAnimator.removeBehavior(pushBehavior)
}
pushBehavior = UIPushBehavior(items: [player], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: -dx, dy: -dy)
pushBehavior.magnitude = distance / maxDistance * 0.75
dynamicAnimator.addBehavior(pushBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: [player] + obstacles)
collisionBehavior.translatesReferenceBoundsIntoBoundary = true
collisionBehavior.collisionDelegate = self
dynamicAnimator.addBehavior(collisionBehavior)
case .cancelled:
isPanning = false
player.center = anchor
default:
break
}
}
}
extension ViewController: UIDynamicAnimatorDelegate {
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator) {
dynamicAnimator.removeAllBehaviors()
player.center = anchor
}
}
extension ViewController: UICollisionBehaviorDelegate {
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item1: UIDynamicItem, with item2: UIDynamicItem, at p: CGPoint) {
print("contact")
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
i´m trying to make my own interactive transition in swift. In my sample project I have two view controllers. With my own transition I can swipe to each other. My problem is: when swiping from the first to the second view controller the second (where I want to swipe in) view controller is appears for a millisecond on the screen, when I did cancel the transition in my pan gesture.
I tried to solve the problem by taking a snapshot from the first view controller and lay it on the transition layer when the transition cancelled. That worked on my iPhone 8, but on the iPhone XS I get the same flash for a millisecond.
The flashing only appears when I swipe very fast over the screen.
Anyone with the same problem.
PangestureHandler:
#IBAction func HandlePan(_ sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.25
let translation = sender.translation(in: view)
var verticalMovement = (translation.y) / view.bounds.height
verticalMovement = verticalMovement - verticalMovement - verticalMovement
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
let movementToStart: CGFloat = 0.0
let interactor = self.interactor
switch sender.state {
case .began:
print(".began")
case .changed:
print("progress: \(progress)")
if progress > movementToStart && !interactor.hasStarted {
interactor.hasStarted = true
performSegue(withIdentifier: "segueToSecondVC", sender: nil)
interactor.update(progress - movementToStart)
}
if interactor.hasStarted {
interactor.update(progress - movementToStart)
interactor.shouldFinish = progress > percentThreshold
}
case .cancelled:
print("cancel")
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
if interactor.shouldFinish {
interactor.finish()
} else {
interactor.cancel()
print("interactor cancel")
}
default:
break
}
}
AnimatorClass:
import UIKit
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private var myView: UIView!
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
func animationEnded(_ transitionCompleted: Bool) {
if !transitionCompleted {
self.myView.alpha = 1.0
}
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to) else { return }
let containerView = transitionContext.containerView
let screenBounds = UIScreen.main.bounds
let startPoint = CGPoint(x: 0, y: UIScreen.main.bounds.height )
toVC.view.frame = CGRect(origin: startPoint, size: screenBounds.size)
toVC.view.clipsToBounds = true
self.myView = fromVC.view.snapshotView(afterScreenUpdates: false)
containerView.addSubview(myView)
self.myView.alpha = 0.0
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
let bottomLeftCorner = CGPoint(x: 0, y: 0)
let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
let duration = transitionDuration(using: transitionContext)
UIView.animate(
withDuration: duration, delay: 10.0, options: .curveLinear,
animations: {
toVC.view.frame = finalFrame
self.myView.removeFromSuperview()
},
completion: { _ in
self.myView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}