I have a view in an iOS project that is meant to be dragged within the main view. The view. I've attached a pan gesture to that view (paletteView), and moving it around works fine.
But, the "glitches" when moves begin. This doesn't happen on the first move, but on subsequent ones. I think what is happening is related to the constraints on the paletteView. It starts located at 100,100 in the main view, but of course whenever a drag ends, unless I change that, it will snap back to that location. So whenever the pan begins, I grab location of the palette, and update the constraints. This also works, but it seems to produce a flash when the next pangesture begins. Here's the main code:
#objc func palettePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
switch sender.state {
case .began:
paletteLeading?.constant = paletteView!.frame.origin.x
paletteTop?.constant = paletteView!.frame.origin.y
paletteView?.setNeedsUpdateConstraints()
case .changed:
paletteView!.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .ended:
print("done \(paletteView!.frame)")
default:
_ = true
}
}
Again, this works fine, but for the brief "flash" of the palette, out of position. I don't know where this would be coming from.
You need to reset the transform at the same time as you update the constraints, at the moment you are updating the constraints to to the new position, but the view is still transformed by the final translation of the previous pan until the first changed pan gesture event comes through.
So your palettePan function would become:
#objc func palettePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
switch sender.state {
case .began:
paletteLeading?.constant = paletteView!.frame.origin.x
paletteTop?.constant = paletteView!.frame.origin.y
paletteView?.setNeedsUpdateConstraints()
paletteView?.transform = .identity // Reset the transform
case .changed:
paletteView!.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .ended:
print("done \(paletteView!.frame)")
default:
_ = true
}
}
Though I would actually update the constraints and transform when the pan gesture ends, rather than when a new one begins:
#objc func palettePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
switch sender.state {
case .changed:
paletteView?.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .ended, .cancelled, .failed:
paletteLeading?.constant = paletteView!.frame.origin.x
paletteTop?.constant = paletteView!.frame.origin.y
paletteView?.setNeedsUpdateConstraints()
paletteView?.transform = .identity // Reset the transform
default:
_ = true
}
}
Related
In my app, I am displaying a popover that shows a custom UIView allowing the user to select a color using various sliders. I also want to implement an 'eyedropper' tool that the user can tap and hold then drag around to choose a color from anything visible in the app. Inside my custom UIView I added a UIPanGestureRecognizer to my button that points to the handlePan method:
var eyedropperStartLocation = CGPoint.zero
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// self is a custom UIView that contains my color selection
// sliders and is placed inside a UITableView that's in
// the popover.
let translation = recognizer.translation(in: self)
if let view = recognizer.view {
switch recognizer.state {
case .began:
eyedropperStartLocation = view.center
case .ended, .failed, .cancelled:
view.center = eyedropperStartLocation
return
default: break
}
view.center = CGPoint(x: view.center.x + translation.x,
y: view.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}
}
I can drag the button around and it changes location, however I have two issues:
The eyedropper button isn't always in front of other items, even inside the popover or the custom UIView inside the popover
The eyedropper button disappears when outside the bounds of the popover
How can I get the button to be visible all the time, including outside the popover? I'll want to detect where the user lets go of it within the app so I can determine what color it was on.
I figured out how to do this so I'll answer my own question. Instead of moving around the view/button that's inside the popup, I create a new UIImageView and add it to the application's Window, letting it span the whole application. The original button stays where it is - you could easily change the state on it to make it look different, or hide it if you wanted to.
You could also use Interface Builder to tie to #IBActions, but I just did everything in code. The clickButton method kicks things off but calculating location in the window and putting it on the screen. The handlePan method does the translation and lets you move it around.
All code below is swift 4 and works in XCode 9.4.1 (assuming I didn't introduce any typos):
// Call this from all your init methods so it will always happen
func commonInit() {
let panner = UIPanGestureRecognizer(target: self, action: #selector(handlePan(recognizer:)))
theButton.addGestureRecognizer(panner)
theButton.addTarget(self, action: #selector(clickButton), for: .touchDown)
eyedropperButton.addTarget(self, action: #selector(unclickButton), for: .touchUpInside)
eyedropperButton.addTarget(self, action: #selector(unclickButton), for: .touchUpOutside)
}
var startLocation = CGPoint.zero
lazy var floatingView: UIImageView = {
let view = UIImageView(image: UIImage(named: "imagename"))
view.backgroundColor = UIColor.blue
return view
}()
// When the user clicks button - we create the image and put it on the screen
// this makes the action seem faster vs waiting for the panning action to kick in
#objc func clickButton() {
guard let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window else { return }
// We ask the button what it's bounds are within it's own coordinate system and convert that to the
// Window's coordinate system and set the frame on the floating view. This makes the new view overlap the
// button exactly.
floatingView.frame = theButton.convert(theButton.bounds, to: nil)
window.addSubview(floatingView)
// Save the location so we can translate it as part of the pan actions
startLocation = floatingView.center
}
// This is here to handle the case where the user didn't move enough to kick in the panGestureRecognizer and cancel the action
#objc func unclickButton() {
floatingView.removeFromSuperview()
}
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self)
switch recognizer.state {
case .ended, .failed, .cancelled:
doSomething()
floatingView.removeFromSuperview()
return
default: break
}
// This section is called for any pan state except .ended, .failed and .cancelled
floatingView.center = CGPoint(x: floatingView.center.x + translation.x,
y: floatingView.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}
ja a gesture that is on the view that vertical black in the photo.
I want to know its location at the level of the screen, because I want to execute a function if for example I slide the view to the right at more than half of the center of the screen
code:
#objc func detectPan(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.startingConstant = self.centerConstraint.constant
case .changed:
let translation = recognizer.translation(in: self.view)
self.centerConstraint.constant = self.startingConstant - translation.x
default:
break
}
}
Fist image
Second image
You can convert coordinates (points and frames) between views as long as they are on the same hierarchy. You have two methods convert(:to:) and convert(:from:).
In your case you seem to want to convert location in your view to screen which is your key window UIApplication.shared.keyWindow.
So in general the point on screen is let pointOnScreen = myView.convert(pointInMyView, to: UIApplication.shared.keyWindow).
So in your case:
let myView = self.view
let pointInMyView = recognizer.locationInView(myView)
let pointOnScreen = myView.convert(pointInMyView, to: UIApplication.shared.keyWindow)
let isViewGestureOnRightSideOfTheScreen = pointOnScreen.x > UIApplication.shared.keyWindow!.frame.midX
you can get point via
func transform(for translation : CGPoint) -> CGAffineTransform {
let moveBy = CGAffineTransform(translationX: translation.x, y: translation.y)
return moveBy
}
#IBAction func setPanGesture(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .changed:
let translation = sender.translation(in: view)
let translationView = transform(for: translation)
print(translationView)
case .ended:
let translation = sender.translation(in: view)
let translationView = transform(for: translation)
print(translationView)
default:
break
}
}
I am trying to implement a transition between two controllers with Hero inside a navigation controller.
I have this method to handle a pan gesture:
#objc func pannedCell(gesture: UIPanGestureRecognizer) {
guard let animatedView = gesture.view as? HotelListingCollectionViewCell else {
return
}
let translation = gesture.translation(in: nil)
let screenHeight = self.rootView.bounds.height
let progress = ((-1 * translation.y) / screenHeight) * 2
print(progress)
switch gesture.state {
case .began:
self.initialViewPostion = animatedView.center
Hero.shared.defaultAnimation = .fade
let vc = ListingSearchViewController()
vc.viewModel.hotels.value = self.viewModel.hotels.value
self.navigationController?.pushViewController(vc, animated: true)
case .changed:
Hero.shared.update(progress)
let pos = CGPoint(
x: animatedView.center.x + translation.x,
y: self.rootView.hotelResultsCollectionView.center.y + translation.y
)
Hero.shared.apply(modifiers: [
.position(pos),
.useGlobalCoordinateSpace,
.useOptimizedSnapshot
], to: animatedView)
default:
if progress > 0.8 {
Hero.shared.finish()
} else {
Hero.shared.cancel()
}
}
}
The problem here is the pushViewController method takes ~ 1 second to execute so when I start dragging my view, it moves under my finger approx. 1 second after I started moving the finger on the screen.
I am doing it wrong ?
Thanks
Ok, I think I found something !
The issue seems to be related to operation I did in .began case of the gesture handler.
I moved the instantiation of the new view controller out of this call back to avoid overloading the UI thread during the animation and everything seems to work as expected.
I am trying to pan 'blockView' with UIPanGestureRecognizer. When the pan is 'Ended', want the 'blockView' to decelerate with inertia, just as a UIScrollView would end scrolling with inertia.
override func viewDidLoad() {
super.viewDidLoad()
self.dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
let collider = UICollisionBehavior(items: [self.magnifiedView!])
collider.collisionDelegate = self
collider.collisionMode = .Boundaries
collider.translatesReferenceBoundsIntoBoundary = true
self.dynamicAnimator.addBehavior(collider)
self.dynamicProperties = UIDynamicItemBehavior(items: [self.magnifiedView!])
//self.dynamicProperties.elasticity = 1.0
//self.dynamicProperties.friction = 0.0
self.dynamicProperties.allowsRotation = false
//self.dynamicProperties.resistance = 10.0
self.dynamicAnimator.addBehavior(self.dynamicProperties)
}
func panBlockView (panGesture: UIPanGestureRecognizer) {
switch panGesture {
case .Began:
self.blockViewLocation = (self.blockView.center)!
case .Changed:
let translation = panGesture.translationInView(self.view)
self.blockView.center = CGPointMake(self.blockViewLocation.x + translation.x, self.self.blockViewLocation.y + translation.y)
case .Ended, .Cancelled:
self.dynamicAnimator.addLinearVelocity(panGesture.velocityInView(self.view), forItem: self.blockView)
}
}
Issue:
As I pan the view, 'blockView' tries to snap to the origin where the pan was originated from. When the pan 'Ends', it creates inertia, but it starts from the origin of the pan (after snapping to pan origin).
NOTE:
Above code works without issues just to PAN THE VIEW.
Adding a Snap Behavior and have it snap fixed the issue.
I have a BoardView class which subclasses UIView. It has several subviews, which all have their own UIPanGestureRecognizer. I use this recognizer to drag around a subview inside my BoardView instance, as such:
func pieceDragged(recognizer: UIPanGestureRecognizer) {
let pieceView = recognizer.view!
if recognizer.state == .Began || recognizer.state == .Changed {
// move the pieceView by the appropriate amount
let translation = recognizer.translationInView(self)
pieceView.center = CGPoint(x: pieceView.center.x + translation.x, y: pieceView.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, inView: self)
} else if recognizer.state == .Ended {
// do something else
}
}
This works fine.
The problem is that I want the subview that is being dragged around to be on top of all of its siblings, so I made my BoardView instance the delegate of each gesture recognizer and overrode this function:
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
let pieceView = gestureRecognizer.view!
bringSubviewToFront(pieceView)
return true
}
This seems to break the entire gesture recognizer: if I attempt to drag aroudn a subview, the pieceDragged(_:) function is called exactly once (where the state of the recognizer is .Began), but no more than that.
If I comment out bringSubviewToFront(pieceView), my code works as before - I can drag around any subview, but it's below some of its siblings.
I'm clueless. What could be going on here?
The problem was that I replaced all my subviews in the layoutSubviews() method (which is automatically called after bringToFront(_:) is called).
Seems like you are going round the houses as well and implementing a method that doesn't really make sense. The delegate method gestureRecognizerShouldBegin(_:) is all about should this recogniser actually start detecting not a place to perform an action.
You already have a suitable place to perform the action you want you could simply update your code to be
func pieceDragged(recognizer: UIPanGestureRecognizer) {
guard let pieceView = recognizer.view else {
return
}
switch recognizer.state {
case .Began:
pieceView.superview?.bringSubviewToFront(pieceView)
case .Changed:
let translation = recognizer.translationInView(self)
recognizer.setTranslation(CGPoint.zero, inView: self)
pieceView.center = CGPoint(x: pieceView.center.x + translation.x, y: pieceView.center.y + translation.y)
default: ()
}
}