Drag behaviour wrong after UIView transformation - ios

I have a custom DraggableView that subclasses UIImageView. I take a photo with the camera, add the resulting UIImage to a DraggableView and then I can happily drag it around the screen, as intended.
Now, if the original photo was taken in landscape, I rotate it using:
if (image?.size.width > image?.size.height)
{
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
}
When I apply this transformation, the drag behaviour still works, but the directions are all wrong - if I drag left, the image moves up, not left. If I drag up, the image moves right, not up.
How do I fix this? I guess it is something to do with the UIPanGestureRecognizer being bound to the non-transformed view?
Edit: Current UIPanGestureRecognizer handler:
func onPhotoDrag(recognizer: UIPanGestureRecognizer?)
{
let translation = recognizer!.translationInView(recognizer?.view)
recognizer!.view!.center = CGPointMake(recognizer!.view!.center.x
+ translation.x, recognizer!.view!.center.y + translation.y);
recognizer?.setTranslation(CGPointZero, inView: recognizer?.view)
if (recognizer!.state == UIGestureRecognizerState.Ended)
{
let velocity = recognizer!.velocityInView(recognizer?.view)
let magnitude = sqrt((velocity.x * velocity.x)
+ (velocity.y * velocity.y))
let slideMult = magnitude / 300;
let slideFactor = 0.1 * slideMult;
let finalPoint = CGPointMake(recognizer!.view!.center.x
+ (velocity.x * slideFactor),
recognizer!.view!.center.y + (velocity.y * slideFactor));
// Animate the drag, and allow the drag delegate to do its work
DraggableView.animateWithDuration(Double(slideFactor),
delay: 0, options: UIViewAnimationOptions.CurveEaseOut,
animations: { recognizer?.view?.center = finalPoint },
completion: {(_) -> Void in self.dragDelegate?.onDragEnd(self)})
} // if: gesture ended
}

Update:
Thanks for posting your code. I pasted your code into my DraggableImageView and reproduced your problem. Although my version was handling the rotated view (without the animation), yours was going sideways.
The difference is that my code asks for the translationInView in the superview of the draggable view. You need to ask for the translationInView and velocityInView in the superview of your draggable view.
Change this line:
let translation = recognizer!.translationInView(recognizer?.view)
to:
let translation = recognizer!.translationInView(recognizer?.view?.superview)
and change this:
let velocity = recognizer!.velocityInView(recognizer?.view)
to:
let velocity = recognizer!.velocityInView(recognizer?.view?.superview)
and all will be happy.
Previous Answer:
Try this version:
class DraggableImageView: UIImageView {
override var image: UIImage? {
didSet {
if (image?.size.width > image?.size.height)
{
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
self.userInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.addTarget(self, action: #selector(draggedView(_:)))
self.addGestureRecognizer(panGestureRecognizer)
}
func moveByDeltaX(deltaX: CGFloat, deltaY: CGFloat) {
self.center.x += deltaX
self.center.y += deltaY
}
func draggedView(sender:UIPanGestureRecognizer) {
if let dragView = sender.view as? DraggableImageView, superview = dragView.superview {
superview.bringSubviewToFront(dragView)
let translation = sender.translationInView(superview)
sender.setTranslation(CGPointZero, inView: superview)
dragView.moveByDeltaX(translation.x, deltaY: translation.y)
}
}
}
Use example:
override func viewDidLoad() {
super.viewDidLoad()
let dragView = DraggableImageView(frame: CGRect(x: 50, y: 50, width: 96, height: 128))
dragView.image = UIImage(named: "landscapeImage.png")
self.view.addSubview(dragView)
}

Related

Completion block of animation is performed immediately

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)
}
}

Sluggish response from custom slider

I am trying to create a custom slider that uses a UIButton as the thumb. Since I can't seem to find a way to use a UIView as the thumb in a slider, I decided to build a custom one. However, when I run it, the thumb is sluggish to respond on the initial drag. The following is the code so far. The gif below is running slower than actual speed but illustrates the point.
import Foundation
import UIKit
class CustomSlider: UIView, UIGestureRecognizerDelegate {
let buttonView = UIButton()
var minimumPosition = CGPoint()
var maximumPosition = CGPoint()
var originalCenter = CGPoint()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
// add a pan recognizer
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(recognizer:)))
recognizer.delegate = self
addGestureRecognizer(recognizer)
backgroundColor = UIColor.green
buttonView.frame = CGRect(x: 0, y: 0, width: bounds.height, height: bounds.height)
buttonView.backgroundColor = UIColor.purple
self.addSubview(buttonView)
minimumPosition = CGPoint(x:buttonView.frame.width / 2, y: 0)
maximumPosition = CGPoint(x: bounds.width - buttonView.frame.width / 2, y: 0)
}
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
originalCenter = buttonView.center
}
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
let newX = originalCenter.x + translation.x
buttonView.center = CGPoint(x: min(maximumPosition.x, max(minimumPosition.x, newX)), y: buttonView.frame.midY)
}
}
}
To make it look smoother, you should reset the translation of the pan and just add it continuously to the center offset:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
// originalCenter = buttonView.center
}
if recognizer.state == .changed {
originalCenter = buttonView.center
let translation = recognizer.translation(in: self)
let newX = originalCenter.x + translation.x
buttonView.center = CGPoint(x: min(maximumPosition.x, max(minimumPosition.x, newX)), y: buttonView.frame.midY)
recognizer.setTranslation(.zero, in: self)
}
}
Also, for making a custom control, you can check out this tutorial:
https://www.sitepoint.com/wicked-ios-range-slider-part-one/
It might be old and in objective-c, but it gives the general idea of overriding UIControl, handling touches and creating a custom control - which you can also extend with #IBDesignable and #IBInspectable.
Hope it helps.
Finally figured it out. It only happens in the iPhone XR simulator. Tried the iPhone XS, among others, and they were smooth as glass.

Draggable UIView Swift 3

I want to be able to drag the objects on the screen, but they wont. I tried everything but still cant.
Here are the code.
func panGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
print("Began.")
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.removeItem(forms[i])
}
}
case .changed:
let translation = gesture.translation(in: forms[1])
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: self.view)
print("\(gesture.view!.center.x)=\(gesture.view!.center.y)")
print("t;: \(translation)")
case .ended:
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.addItem(forms[i])
}
}
print("Ended.")
case .cancelled:
print("Cancelled")
default:
print("Default")
}
}
Also they have gravity. The forms are squares and circles.
Explanation:
in .began - i disable the gravity for selected form.
in .changed - i try to change the coordinates.
in .end - i enable again gravity.
ScreenShot.
Step 1 : Take one View which you want to drag in storyBoard.
#IBOutlet weak var viewDrag: UIView!
Step 2 : Add PanGesture.
var panGesture = UIPanGestureRecognizer()
Step 3 : In ViewDidLoad adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
panGesture = UIPanGestureRecognizer(target: self, action: #selector(ViewController.draggedView(_:)))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 4 : Code for draggedView.
func draggedView(_ sender:UIPanGestureRecognizer){
self.view.bringSubview(toFront: viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Step 5 : Output.
It's very easy if you subclass a view:
DraggableView...
class DraggableView: UIIView {
var fromleft: NSLayoutConstraint!
var fromtop: NSLayoutConstraint!
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
fromleft = constraint(id: "fromleft")!
fromtop = constraint(id: "fromtop")!
}
}
override func common() {
super.common()
let p = UIPanGestureRecognizer(
target: self, action: #selector(drag))
addGestureRecognizer(p)
}
#objc func drag(_ s:UIPanGestureRecognizer) {
let t = s.translation(in: self.superview)
fromleft.constant = fromleft.constant + t.x
fromtop.constant = fromtop.constant + t.y
s.setTranslation(CGPoint.zero, in: self.superview)
}
}
Drop a UIView in your scene.
As normal, add a constraint from the left (that's the x position) and add a constraint from the top (that's the y position).
In storyboard simply simply name the constraints "fromleft" and "fromtop"
You're done.
It now works perfectly - that's it.
What is that handy constraint( call ?
Notice the view simply finds its own constraints by name.
In Xcode there is STILL no way to use constraints like IBOutlets. Fortunately it is very easy to find them by "identifier". (Indeed, this is the very purpose of the .identifier feature on constraints.)
extension UIView {
func constraint(id: String) -> NSLayoutConstraint? {
let cc = self.allConstraints()
for c in cc { if c.identifier == id { return c } }
//print("someone forgot to label constraint \(id)") //heh!
return nil
}
func allConstraints() -> [NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { c in
return c.firstItem as? UIView == self ||
c.secondItem as? UIView == self
}
}
Tip...edge versus center!
Don't forget when you make the constraints on a view (as in the image above):
you can set the left one to be either:
to the left edge of the white box, or,
to the center of the white box.
Choose the correct one for your situation. It will make it much easier to do calculations, set sliders, etc.
Footnote - an "initializing" UIView, UI "I" View,
// UI "I" View ... "I" for initializing
// Simply saves you typing inits everywhere
import UIKit
class UIIView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
common()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
common()
}
func common() { }
}
Use below code for Swift 5.0
Step 1 : Take one UIView from Storyboard, drag it into your ViewController file and Create IBOutlet of UIView.
#IBOutlet weak var viewDrag: UIView!
var panGesture = UIPanGestureRecognizer()
Step 2 : In viewDidLoad() adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action:(Selector(("draggedView:"))))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 3 : Create func and add code to move the UIView as like below.
func draggedView(sender:UIPanGestureRecognizer){
self.view.bringSubviewToFront(viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Hope this will help someone.
This UIView extension makes a UIView object draggable and limits the movement to stay within the bounds of the screen.
extension UIView {
func makeDraggable() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.addGestureRecognizer(panGesture)
}
#objc func handlePan(_ gesture: UIPanGestureRecognizer) {
guard gesture.view != nil else { return }
let translation = gesture.translation(in: gesture.view?.superview)
var newX = gesture.view!.center.x + translation.x
var newY = gesture.view!.center.y + translation.y
let halfWidth = gesture.view!.bounds.width / 2.0
let halfHeight = gesture.view!.bounds.height / 2.0
// Limit the movement to stay within the bounds of the screen
newX = max(halfWidth, newX)
newX = min(UIScreen.main.bounds.width - halfWidth, newX)
newY = max(halfHeight, newY)
newY = min(UIScreen.main.bounds.height - halfHeight, newY)
gesture.view?.center = CGPoint(x: newX, y: newY)
gesture.setTranslation(CGPoint.zero, in: gesture.view?.superview)
}
}

How do I add a drag up menu to my app (similar to the control center except it covers up the whole screen)?

The following code is meant to cause a view that is hidden below the main view to be able to be dragged from the bottom of the screen completely over the main view. For some reason, the code below does absolutely nothing. Any suggestions are highly appreciated. Thanks!
import UIKit
class ControlMenuView: FXBlurView {
var animator:UIDynamicAnimator!
var container:UICollisionBehavior!
var snap: UISnapBehavior!
var dynamicItem: UIDynamicItemBehavior!
var gravity:UIGravityBehavior!
var panGestureRecognizer: UIPanGestureRecognizer!
func setup () {
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ControlMenuView.handlePan(_:)))
panGestureRecognizer.cancelsTouchesInView = false
self.addGestureRecognizer(panGestureRecognizer)
animator = UIDynamicAnimator(referenceView: self.superview!)
dynamicItem = UIDynamicItemBehavior(items: [self])
dynamicItem.allowsRotation = false
dynamicItem.elasticity = 0
gravity = UIGravityBehavior(items: [self])
gravity.gravityDirection = CGVectorMake(0, -1)
container = UICollisionBehavior(items: [self])
configureContainer()
animator.addBehavior(gravity)
animator.addBehavior(dynamicItem)
animator.addBehavior(container)
}
func configureContainer (){
let boundaryWidth = UIScreen.mainScreen().bounds.size.width
container.addBoundaryWithIdentifier("upper", fromPoint: CGPointMake(0, self.frame.size.height), toPoint: CGPointMake(boundaryWidth, self.frame.size.height))
let boundaryHeight = UIScreen.mainScreen().bounds.size.height
container.addBoundaryWithIdentifier("lower", fromPoint: CGPointMake(0, boundaryHeight), toPoint: CGPointMake(boundaryWidth, boundaryHeight))
}
func handlePan (pan:UIPanGestureRecognizer){
let velocity = pan.velocityInView(self.superview).y
var movement = self.frame
movement.origin.x = 0
movement.origin.y = movement.origin.y + (velocity * 0.05)
if pan.state == .Ended {
panGestureEnded()
}else if pan.state == .Began {
snapToTop()
}else{
animator.removeBehavior(snap)
snap = UISnapBehavior(item: self, snapToPoint: CGPointMake(CGRectGetMidX(movement), CGRectGetMidY(movement)))
animator.addBehavior(snap)
}
}
func panGestureEnded () {
animator.removeBehavior(snap)
let velocity = dynamicItem.linearVelocityForItem(self)
if fabsf(Float(velocity.y)) > 250 {
if velocity.y < 0 {
snapToBottom()
}else{
snapToTop()
}
}else{
if let superViewHeigt = self.superview?.bounds.size.height {
if self.frame.origin.y > superViewHeigt / 2 {
snapToTop()
}else{
snapToBottom()
}
}
}
}
func snapToBottom() {
gravity.gravityDirection = CGVectorMake(0, 2.5)
}
func snapToTop(){
gravity.gravityDirection = CGVectorMake(0, -2.5)
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.tintColor = UIColor.clearColor()
}
}
The following code is meant to cause a view that is hidden below the
main view to be able to be dragged from the bottom of the screen
completely over the main view.
You mention that the view is "hidden below the main view." Have you checked to see if the pan gesture target selector is even being called?

snapchat-like pinch zoom on iOS

I've published iOS app in swift whose main functions are:
1) add a photo / take a photo
2) add emoji on the photo
3) zoom, rotate, drag emoji to decorate photo
4) share it on instagram.
Emojis can be rotated, zoomed, and dragged. I've implemented these functions using UIGestureRecognizers such as UIRoationGestrueRecognizer, UIPinchGestureRecognizer, and UIPanGesstureRecognizer.
Now I am trying to update app with snapchat-like pinch zoom feature where users can zoom in / out emojis between two fingers to the extreme. Current pinch gesture works only when users' fingers are on the imageView (emoji).
Any idea / example code how to do snapchat-like pinch zoom? Below codes are how I handled rotation, pinch, and drag. Thanks in advance.
// UI Gesture Recognizers
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
if(deleteMode) {
return
}
if let view = recognizer.view {
view.transform = CGAffineTransformScale(view.transform,
recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
}
#IBAction func handleRotate(recognizer : UIRotationGestureRecognizer) {
if(deleteMode) {
return
}
if let view = recognizer.view {
view.transform = CGAffineTransformRotate(view.transform, recognizer.rotation)
recognizer.rotation = 0
}
}
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
if(deleteMode) {
return
}
let translation = recognizer.translationInView(self.view)
var centerX: CGFloat!
var centerY: CGFloat!
if let view = recognizer.view {
// limit the boundary - using backgroundPanel.frame.width, height, origin.x, origin.y
if(view.center.x + translation.x < panelBackground.frame.origin.x) {
centerX = view.center.x + translation.x + 10
} else if(view.center.x > panelBackground.frame.size.width){
centerX = view.center.x + translation.x - 10
} else {
centerX = view.center.x + translation.x
}
if(view.center.y < panelBackground.frame.origin.y - 60){
// set y that I can use below
centerY = view.center.y + translation.y + 10
} else if(view.center.y > panelBackground.frame.size.height){
centerY = view.center.y + translation.y - 10
} else {
centerY = view.center.y + translation.y
}
// set final position
view.center = CGPoint(x:centerX,
y:centerY)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
}
#IBAction func handleLongPress(recognizer: UILongPressGestureRecognizer) {
if(recognizer.state == UIGestureRecognizerState.Began) {
if(!deleteMode) {
print("LongPress - Delete Shows")
for (_, stickers) in self.backgroundImage.subviews.enumerate() {
for (_, deleteButtons) in stickers.subviews.enumerate() {
if let delete:UIImageView = deleteButtons as? UIImageView{
if(delete.accessibilityIdentifier == "delete") {
delete.alpha = 0.5
}
}
}
}
deleteMode = true
} else {
deleteButtonHides()
}
}
}
I'm also looking drag, pan and zoom at the same time like snapchat but if you are just looking for zoom. I'm using below function for a label to zoom via pinch. It is not smooth but do the zooming job.
func handlePinch(recognizer: UIPinchGestureRecognizer) {
if let view = recognizer.view as? UILabel {
let pinchScale: CGFloat = recognizer.scale
view.transform = view.transform.scaledBy(x: pinchScale, y: pinchScale)
recognizer.scale = 1.0
}
}
For drag,pan and zoom at the same time, checkout my below post:
Pinch, drag and pan at the same time
To achieve this snapchat like pinch zooming, add pinch gesture on Parent view also and instead of recognizer transform the selected sticker as written below:
#objc func mainImgPinchGesture(_ recognizer: UIPinchGestureRecognizer) {
print("----pinchGestureAction")
if let view = recognizer.view {
if selectedSubView != nil{
self.selectedSubView.transform = view.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
self.selectedSubView.contentScaleFactor = 1
}
}
}

Resources