I have a UIView that I can drag and drop along the Y axis that snaps into 1 of 2 positions. The issue is that I have to drag it over a certain point in the Y axis in order for it to snap into place. This feels clunky when you try to quickly swipe but don't make it past that specific Y axis and it snaps back to the start position.
My desired goal is to have it snap into 1 of 2 positions depending on the direction the user was swiping when he released the UIView.
#objc func panGesture(recognizer: UIPanGestureRecognizer) {
let window = UIApplication.shared.keyWindow
let translation = recognizer.translation(in: self)
let currentY = self.frame.minY
let partialY = (window?.frame.height)!-194
let halfWayPoint = ((window?.frame.height)!/2)-40
self.frame = CGRect(x: 0, y: currentY + translation.y, width: self.frame.width, height: self.frame.height)
recognizer.setTranslation(CGPoint.zero, in: self)
switch recognizer.state {
case .changed:
// Prevent scrolling up past y:0
if currentY <= 0
{
self.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
}
case .ended:
// Snap to 80 (y) (expanded)
if currentY < 80 || currentY > 80 && currentY < halfWayPoint
{
UIView.animate(withDuration:0.62, delay: 0.0, usingSpringWithDamping: 0.62, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations:
{
recognizer.view!.frame = CGRect(x: 0, y: 80, width: self.frame.width, height: self.frame.height)
}, completion: { (true) in
self.isExpanded = true
})
}
// Snap back to partial (y) (original position)
if currentY > partialY || currentY < partialY && currentY > halfWayPoint
{
UIView.animate(withDuration:0.62, delay: 0.0, usingSpringWithDamping: 0.62, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations:
{
recognizer.view!.frame = CGRect(x: 0, y: partialY, width: self.frame.width, height: self.frame.height)
}, completion: {(true) in
self.isExpanded = false
})
}
default:
print("Default")
}
As you can see halfWayPoint is the point in which the view must be over or under in order to determine the position it snaps into. This is ineffective and I would like to do it depending on the direction (up or down) the user is dragging the view.
I suggest you use UIPanGestureRecognizers velicoty(in:) method.
It gives you the speed of the swipe (in points per second).
If you just add it to the currentY (preferably scaled with some constant), you will get that flicky feeling, and gesture will feel more natural.
Just replace
let currentY = self.frame.minY with let currentY = self.frame.minY + recognizer.velocity(in: nil).y * someConstant and play with someConstant until you get the feeling you want.
Related
I want my swift code to follow the gif I created. What is going is after the first animation there is not any change of the position. Like the gif I created it should first move down then move up again. I tried multiplying by the negative -0.15 thinking it would go in the north direction. Right now it is not having a effect.
UIView.animate(withDuration: 1, animations: {
self.block1.frame = CGRect(x: 0, y: self.view.frame.height * 0.15, width: self.view.frame.width, height: self.view.frame.height * 0.1)
self.block1.center = self.view.center
}) { done in
/// first animation finished, start second
if done {
UIView.animate(withDuration: 1, animations: {
self.block1.frame = CGRect(x: 0, y: self.view.frame.height * (-0.15), width: self.view.frame.width, height: self.view.frame.height * 0.1)
self.block1.center = self.view.center
})
}
}
Start by getting rid of self.block1.center = self.view.center as you're making the centre point of the block the same as the view in both cycles.
self.view.frame.height * (-0.15) is setting an absolute position out side of the view, not sure if that's intentional, but you might want to be aware of it. Instead, if you only wanted to move a given distance, say the height of the view, you should be using a relative position, such as block1.frame.origin.y -= block.frame.size.height (probably a much easier way to say that, but you get the idea)
So I created a really quick Playground, made up some values and got a "sort of" bouncy, returny thing going as an example
import UIKit
import XCPlayground
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 600.0))
// XCPShowView("container", view: container) // -> deprecated
let block1 = UIView(frame: CGRect(x: 0.0, y: 0.0, width: (view.frame.width) / 2, height: 150))
block1.backgroundColor = UIColor.green
block1.center.x = view.center.x
view.addSubview(block1)
UIView.animate(withDuration: 1, animations: {
let y = view.frame.height * 0.15
print(y)
block1.center.y = view.center.y
}) { done in
print("Done = \(done)")
/// first animation finished, start second
if done {
UIView.animate(withDuration: 1, animations: {
block1.frame.origin.y = 0
})
}
}
XCPlaygroundPage.currentPage.liveView = view
Caveat: There's probably a really neat and easy way to do "auto reversal" style animations using CALayer and I'd be keen to see other examples presenting the same concept
I'm having a hard time with animations and PanGestureRecognizers, I want a view that slides by dragging up or dawn with your finger. It has 4 parts
1 - 2 the height, width and bottom constraints need to be changed
2 - 3 the height is the maximum height and you can slide it in or out
3 - 4 the card is at its maximum height and you can swipe it down
here is an image for a better understanding
thanks!
this is how I would do it.
var theView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// First create your view.
theView = UIView(frame: CGRect(x: 40, y: self.view.bounds.height - 120, width: self.view.frame.width - 80, height: 100))
// set its background color
theView.backgroundColor = UIColor.black
// Create the round corners
theView.layer.cornerRadius = 20
// And add it to the view
self.view.addSubview(theView)
// Create the UIPanGestureRecognizer and assign the function to the selector
let gS = UIPanGestureRecognizer(target: self, action: #selector(growShink(_:)))
// Enable user interactions on the view
theView.isUserInteractionEnabled = true
// Add the gesture recognizer to the view
theView.addGestureRecognizer(gS)
}
// The control value is used to determine wether the user swiped up or down
var controlValue:CGFloat = 0
var animating = false
// The function called when the user swipes
#objc func growShink(_ sender:UIPanGestureRecognizer){
let translation = sender.location(in: theView)
// Check the control value to see if it has been set
if controlValue != 0 && !animating{
// if it has been set check that the control value is greater than the new value recieved by the pan gesture
if translation.x < controlValue{
animating = true
// If it is, change the values of the frame using animate
UIView.animate(withDuration: 0.5, animations: {
self.theView.frame = CGRect(x: 0, y: self.view.bounds.height - 300, width: self.view.frame.width, height: 300)
}, completion: {finished in
UIView.animate(withDuration: 1.0, animations: {
self.theView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
}, completion: {finished in self.animating = false; self.controlValue = 0})})
}else if translation.x > controlValue{
animating = true
// else, change the values of the frame back.
UIView.animate(withDuration: 0.5, animations: {
self.theView.frame = CGRect(x: 0, y: self.view.bounds.height - 300, width: self.view.frame.width, height: 300)
}, completion: {finished in
UIView.animate(withDuration: 1.0, animations: {
self.theView.frame = CGRect(x: 40, y: self.view.bounds.height - 120, width: self.view.frame.width - 80, height: 100)
}, completion: {finished in self.animating = false; self.controlValue = 0})})
}
}
// set the control value to the value received from the pan gesture
controlValue = translation.x
}
}
You can adjust the width and height values however you want.
I'm trying to get a view to shrink down to nothing in the bottom right corner when its cancel button is pressed. Currently when i use:
UIView.animate(withDuration: 0.7, delay: 0, options: .beginFromCurrentState, animations: {
self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
})
the view shrinks into its center.
I understand that I should be setting the anchor point of the layer so it shrinks to that point but when I do this it moves the view center to that point then shrinks:
UIView.animate(withDuration: 0.7, delay: 0, options: .beginFromCurrentState, animations: {
self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
})
Any ideas?
Edit: More info would be that im using cartography to set the view's constraints to the viewController.view edges, I'm wondering if this has something to do with it?
Frame of UIView or CALayer are actually derived properties which are calculated from layers position and anchorpoint. So, when you change either one of these, the layer frame also changes and hence you see the effect of view movement.
When you change the anchor point of the CALayer, you could also immediately set the position of the layer to compromise for the change. You can offset the view position with amount of anchor point changes relative to views bounds.
Here is a simple extension on UIView, which you could use to change the layers anchor point without actually changing the position.
extension UIView {
func set(anchorPoint: CGPoint) {
let originalPosition = CGPoint(x: frame.midX, y: frame.midY)
let width = bounds.width
let height = bounds.height
let newXPosition = originalPosition.x + (anchorPoint.x - 0.5) * width
let newYPosition = originalPosition.y + (anchorPoint.y - 0.5) * height
layer.anchorPoint = anchorPoint
layer.position = CGPoint(x: newXPosition,
y: newYPosition)
}
}
The code above changes position of CALayer as soon as you make change to the anchorPoint. You could as well do this without having to change the position.
Whenever you set the anchorPoint, simply set back previous frame,
let originalFrame = view.frame
view.layer.anchorPoint = CGPoint(x: 0, y: 0)
view.frame = originalFrame
This will prevent your view from moving and stay at the same position, despite of the change in anchorPoint.
Also, for your particular case, setting anchorPoint to layer triggers intrinsic animation. So, if you want to make it like shrink to lower or upper edge, perform anchorPoint change before animation like so,
UIView.performWithoutAnimation {
self.set(anchorPoint: CGPoint(x: 0, y: 0))
}
UIView.animate(withDuration: 0.7, delay: 0, options: .beginFromCurrentState, animations: {
self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
})
I'm rotating 360º a UIButton using this code:
self.btn.transform = CGAffineTransform(rotationAngle: (180.0 * CGFloat(M_PI)) / 180.0)
self.btn.transform = CGAffineTransform(rotationAngle: (0.0 * CGFloat(M_PI)) / 180.0)
The problem is that it rotates from left to right and i want to invert the direction.
I have tried using -M_PI, or -180 and other combinations, but without success.
What am i doing wrong?
You can apply a negative transform to reverse the direction:
self.btn.transform = CGAffineTransform(rotationAngle: -0.999*CGFloat.pi)
Undo it with:
self.btn.transform = CGAffineTransform.identity
Remember that the transform mutates the view but not the actual object. You don't have to "undo" it, you just replace it.
Here is some simple working playground code showing animation counter-clockwise:
import UIKit
import PlaygroundSupport
let view:UIView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = UIColor.red
PlaygroundPage.current.liveView = view
let button = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
button.backgroundColor = UIColor.white
view.addSubview(button)
UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, options: .repeat, animations: {
let rotation = -0.999*CGFloat.pi
button.transform = CGAffineTransform(rotationAngle: rotation)
print(rotation)
}, completion: nil)
The result is this:
I have a view controller that has a UIView that I call playerView. In playerView, I use an AVLayer and AVPlayerLayer to show the user a video. Image the Youtube app when you first click on any video.
How do I manipulate the frame of this playerView so that it can take up the entire screen. (Go full screen)
This is the current frame:
//16 x 9 is the aspect ratio of all HD videos
let height = view.frame.width * 9 / 16
let playerFrame = CGRect(x: 0, y: 0, width: view.frame.width, height: height)
playerView.frame = videoPlayerFrame
I was testing around in the YouTube app and their app only supports Portrait orientation (just like mine) due to how the animation of a video going full screen looks. How can I do this at the the tap of a button and even further when the user holds the device horizontally?
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.playerView.frame = ?
}, completion: nil)
Edit: Tried this but it doesn't really work how I want it... it doesn't take up the whole screen
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.playerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.height, height: self.view.frame.width)
self.playerView.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
}, completion: nil)
I figured it out.
I create a global variable called isExpanded that will be set to true when we're in full screen mode. Also, I create a CGPoint variable called videoPlayerViewCenter so I can save the center before going full screen so I can set it again when going back to small screen.
This is the code that gets called when the user wants to go full screen
func fullScreenTapped(sender: vVideoPlayer) {
if !isExpanded {
//Expand the video
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.videoPlayerViewCenter = self.videoPlayerView.center
self.videoPlayerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.height, height: self.view.frame.width)
self.videoPlayerView.center = self.view.center
self.videoPlayerView.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
self.videoPlayerView.layoutSubviews()
}, completion: nil)
} else {
//Shrink the video again
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
//16 x 9 is the aspect ratio of all HD videos
self.videoPlayerView.transform = CGAffineTransform.identity
self.videoPlayerView.center = self.videoPlayerViewCenter
let height = self.view.frame.width * 9 / 16
let videoPlayerFrame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: height)
self.videoPlayerView.frame = videoPlayerFrame
self.videoPlayerView.layoutSubviews()
}, completion: nil)
}
isExpanded = !isExpanded
}
To expand the video, I save the center in my global CGPoint variable and then set the frame. Then, I set the center and do a 90 degree turn using CGAffineTransform.
To shrink the video again, I basically set the settings to how they were before. (The order in which things are done is very important)
Another thing that's also very important is to override your layoutSubviews method in your videoPlayerView to be like this:
override func layoutSubviews() {
super.layoutSubviews()
self.playerLayer?.frame = self.bounds
}
This will make it so your playerLayer adjusts its frame as the view is getting bigger.
I know is too late but maybe someone else can find this code helpful.
I use to use Auto layout constraints in my views so animating must have a different approach.
func setMapFullScreen(_ fullscreen: Bool) {
if fullscreen {
// Set the full screen constraints
self.setFullScreenLayoutConstraints()
// Hide the navigation bar
self.navigationController?.setNavigationBarHidden(true, animated: true)
} else {
// Set small screen constraints
self.setSmallScreenLayoutConstraints()
// Show the navigation bar
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
// Animate to full screen
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}) { (finished) in
// ...
}
In my case a have I MKMapView and I want to tap on it in order to see the map fullscreen (and back).
This is the function that toogle the state.
As you can see, in the UIView.animate call, I animate only the self.view.layoutIfNeeded()
I also hide the navigation bar cause I have one ☺
Here my setSmallScreenLayoutConstraints and setFullScreenLayoutConstraints functions using SnapKit framework
func setSmallScreenLayoutConstraints() {
self.mapView.snp.remakeConstraints { (make) -> Void in
make.top.equalTo(self.mapLabel.snp.bottom).offset(8)
make.left.equalTo(self.view).offset(0)
make.right.equalTo(self.view).offset(0)
make.height.equalTo(150)
}
}
func setFullScreenLayoutConstraints() {
self.mapView.snp.remakeConstraints { (make) -> Void in
make.top.equalTo(self.view).offset(0)
make.left.equalTo(self.view).offset(0)
make.right.equalTo(self.view).offset(0)
make.bottom.equalTo(self.view).offset(0)
}
}
This is the result
Enjoy!!
As I said it is not an answer for your question. It is an idea and hope it helps you:
var rectangleView:UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Tap Gesture Recognizer for growing the rectangleView
let tapForward = UITapGestureRecognizer.init(target: self, action: #selector(self.tapForward(tap:)))
tapForward.numberOfTapsRequired = 1
// Configure the rectangleView
self.rectangleView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height))
self.rectangleView.transform = CGAffineTransform.init(scaleX: 0.65, y: 0.25)
self.rectangleView.backgroundColor = UIColor.red
self.rectangleView.addGestureRecognizer(tapForward)
self.view.addSubview(rectangleView)
}
And this going to be our tapForward(tap:) function:
func tapForward(tap:UITapGestureRecognizer) {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: .curveEaseInOut, animations: {
self.rectangleView.transform = CGAffineTransform.identity
}) { (completed:Bool) in
// Completion block
}
}