Drag and drop - UIGestureRecognizer - View added not where I want to - ios

I have this 2 tableViews (All Services available and service offered) in a splitViewController. The idea is that then the cell is moved across the center of the screen, it moves to the service offered table view.
Everything works except then the touch `.began, the cell is being added to the top of the tableView covering the navBar.
On .changed it follows my finger
I would like the view/cell to be added where the touch began, on top of the "ServiceCell" I am touching. I tried adding it to the splitViewController.view but I think there is something wrong with my logic.
Here the code:
func didLongPressCell (gr: UILongPressGestureRecognizer) {
let serviceCell:ServiceCell = gr.view as! ServiceCell
switch gr.state {
case .began:
let touchOffsetInCell = gr.location(in: gr.view)
let dragEvent = DragganbleServiceCell()
mDraggableServiceCell = dragEvent.makeWithServiceCell(serviceCell: serviceCell, offset: self.tableView.contentOffset, touchOffset: touchOffsetInCell)
self.splitViewController?.view.addSubview(mDraggableServiceCell!)
case .changed:
let cp:CGPoint = gr.location(in: self.view)
let newOrigin = CGPoint(x: (cp.x), y: (cp.y) - (mDraggableServiceCell?.touchOffset?.y)!)
UIView.animate(withDuration: 0.1, animations: {
self.mDraggableServiceCell?.frame = CGRect(origin: newOrigin, size: (self.mDraggableServiceCell?.frame.size)!)
})
case .ended:
let detailViewNC = self.splitViewController?.viewControllers[1] as! UINavigationController
let detailView = detailViewNC.topViewController as! ServiceOfferedTableViewController
if (mDraggableServiceCell?.frame.intersects(detailView.tableView.frame))! {
onDragEnded(serviceCell:serviceCell)
}else{
mDraggableServiceCell?.removeFromSuperview()
}
default:
print("Any other action?")
}
}
In DraggableServiceCell:
class DragganbleServiceCell: ServiceCell {
var originalPosition:CGPoint?
var touchOffset:CGPoint?
func makeWithServiceCell(serviceCell:ServiceCell, offset:CGPoint, touchOffset:CGPoint)->DragganbleServiceCell{
let newFrame = CGRect(x: serviceCell.frame.origin.x, y: serviceCell.frame.origin.y, width: serviceCell.frame.size.width, height: serviceCell.frame.size.height)
let dragCell = DragganbleServiceCell(frame: newFrame)
dragCell.touchOffset = touchOffset
dragCell.service = serviceCell.service
dragCell.serviceName.text = serviceCell.service?.service_description
return dragCell
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.containingView.backgroundColor = UIColor.rgb(230, green: 32, blue: 31)
self.serviceName.textColor = .white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Your problem is in makeWithServiceCell(serviceCell:ServiceCell, offset:CGPoint, touchOffset:CGPoint) -> DragganbleServiceCell. The frame of your serviceCell is relative to the tableView that contains it instead of UISplitViewController's view and you are never using the offset or touchOffset values to correct that. So try adjusting the frame in the same way you are doing it in the .changed event.
You might even find that the convert(_:to:) method on UIView might be useful to do CGPoint transformations between two views. See more details here.

I would suggest adding the gesture to self.splitViewController?.view instead of self.viewif you are not doing and changing this part of the code.
case .changed:
let cp:CGPoint = gr.location(in: self.view)
let newOrigin = CGPoint(x: (cp.x), y: (cp.y) - (mDraggableServiceCell?.touchOffset?.y)!)
UIView.animate(withDuration: 0.1, animations: {
self.mDraggableServiceCell?.frame = CGRect(origin: newOrigin, size: (self.mDraggableServiceCell?.frame.size)!)
})
to this
case .changed:
let cp:CGPoint = gr.location(in: self.splitViewController?.view)
UIView.animate(withDuration: 0.1, animations: {
self.mDraggableServiceCell?.center = cp
})
try once.

Related

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.

Adding a UIPanGestureRecognizer to UITableViewCell causes odd behaviour in iPhone X Landscape

I have some code in my HW Cell as seen below. I am leaving out the extra code that appears to be largely unrelated.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.recognizer = UIPanGestureRecognizer(target: self, action: #selector(HWCell.handlePan(_:)))
self.recognizer.delegate = self
addGestureRecognizer(self.recognizer)
}
override func awakeFromNib() {
super.awakeFromNib()
self.cardView.layer.masksToBounds = false
self.layer.masksToBounds = false
self.contentView.layer.masksToBounds = false
self.layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
self.cardView.layoutIfNeeded() //Needed for iOS10 since layoutSubviews() reports inaccurate frame sizes.
self.cardView.layer.cornerRadius = self.cardView.frame.size.height / 3
self.cardView.layer.masksToBounds = false
}
//MARK: - horizontal pan gesture methods
func handlePan(_ recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .began {
// when the gesture begins, record the current center location
originalCenter = self.center
//originalCenter = self.cardView.center
self.bringSubview(toFront: self.contentView)
self.originalAlpha = self.cardView.alpha
}
// 2
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
self.center = CGPoint(x: originalCenter.x + translation.x, y: originalCenter.y)
// 3 //Remembered, state == .Cancelled when recognizer manually disabled.
if recognizer.state == .ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if (!deleteOnDragRelease && !completeOnDrag) {
// if the item is not being deleted, snap back to the original location
UIView.animate(withDuration: 0.2, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: { self.frame = originalFrame }, completion: nil)
if (self.task?.completed == false) {
UIView.transition(with: self.completionImageView,
duration: 0.2,
options: UIViewAnimationOptions.transitionCrossDissolve,
animations: { self.completionImageView.image = UIImage(named: "Grey Checkmark") },
completion: nil)
}
}
What happens is that, and keep in mind that this ONLY HAPPENS on iPhone X in Landscape mode, is the cell extends itself further by stretching instead of simply moving when I drag my finger across it.
You can see this behaviour in a video I uploaded: https://www.youtube.com/watch?v=qPXZWwnuWhU&feature=youtu.be
This odd behaviour never occurs in iOS10, only in iOS 11, on this particular device and orientation.
My constraints are blue and 0 warnings/errors, I played around with them to see if the safe area was related to this problem. The problem is gone only if I set the leading/trailing constraints of the UITableView to line up with the Safe Area. If they are lined up with the Superview, this error occurs.
Anyone have an idea what could be causing this?

turn off a pangesturderecognizer with a double tapgesturerecognizer and a swift flow

I have a button on my main UI that I can create views programmatically with. That is down at the bottom. They come out as a green circle that I can move around using a PanGestureRecognizer.
My goal is to turn off the PanGestureRecognizer with a TapGestureRecognizer (I would like it to be a double tap) essentially locking that specific view in place once I double tap it.
This is the code that I have so far. When I tap the view it does in fact change color, but I cannot figure out how to reference the PanGestureRecognizer to disable it.
class MakeACircle: UIView {
var circleCenter = CGPoint()
let tap = UITapGestureRecognizer()
let panGesture = UIPanGestureRecognizer()
var checkSomething = 1
init() {
super.init(frame: CGRect(x: 0.0, y: 0.0, width: 100, height: 100))
self.layer.cornerRadius = 50.0
self.backgroundColor = UIColor.green
self.isUserInteractionEnabled = true
UITapGestureRecognizer().numberOfTapsRequired = 2
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dragCircle(gesture: UIPanGestureRecognizer) {
let target = gesture.view!
switch gesture.state {
case .began, .ended:
circleCenter = target.center
case .changed:
let translation = gesture.translation(in: self.superview)
target.center = CGPoint(x: circleCenter.x + translation.x, y: circleCenter.y + translation.y)
default: break
}
}
func freeze(gesture: UITapGestureRecognizer) {
switch checkSomething {
case 1:
self.panGesture.isEnabled = false
self.backgroundColor = UIColor.blue
checkSomething += 1
print(checkSomething)
case 2:
self.panGesture.isEnabled = true
self.backgroundColor = UIColor.red
checkSomething -= 1
print(checkSomething)
default:
break
}
}
}
I am creating the view programmatically with this IBButton
#IBAction func makeACircle(_ sender: UIButton) {
let something = MakeACircle()
self.view.addSubview(something)
something.addGestureRecognizer(UIPanGestureRecognizer(target: something, action: #selector(something.dragCircle(gesture:))))
something.addGestureRecognizer(UITapGestureRecognizer(target: something, action: #selector(something.freeze(gesture:))))
something.center = self.view.center
}
The taps work, as you can see I have a switch in there to turn it colors.. it prints the number and turns color.. but it happens with just one tap and the pan stays on.
Any insight is greatly appreciated! This is my first execution of this, I know I'm right at the door, but I just can't figure it out.
It looks like you're not storing a reference to self.panGesture on your MakeACircle() object when you add it, so when you try to disable it, it doesn't do anything. Where is self.panGesture being set?
instead of this:
something.addGestureRecognizer(UIPanGestureRecognizer(target: something, action: #selector(something.dragCircle(gesture:))))
try this:
something.panGesture = UIPanGestureRecognizer(target: something, action: #selector(something.dragCircle(gesture:)))
something.addGestureRecognizer(something.panGesture)
UPDATE
for your tap gesture recognizer, make sure you're also storing a reference to it in the same way you are with the panGesture.
in your init(), you have:
UITapGestureRecognizer().numberOfTapsRequired = 2
but that won't do anything because it's not the same tap gesture you added when you said
something.addGestureRecognizer(UITapGestureRecognizer(...))
So you should do something like you did with the other gesture
something.tap = UITapGestureRecognizer(target: something, action: #selector(whatever))
something.tap.numberOfTapsRequired = 2
something.addGestureRecognizer(something.tap)

Drag behaviour wrong after UIView transformation

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

Cannot invoke initializer for type UIPanGestureRecognizer

I'm following this tutorial on gesture driven apps, http://www.raywenderlich.com/77974/making-a-gesture-driven-to-do-list-app-like-clear-in-swift-part-1
however when I come to implement the recognizer I receive the error
Cannot invoke initializer for type 'UIPanGestureRecognizer' with an argument list of type '(target: NSObject -> () -> TableViewCell, action: String)'
Swift version:
Apple Swift version 2.0 (swiftlang-700.0.59 clang-700.0.72)
Target: x86_64-apple-darwin14.5.0
import UIKit
import QuartzCore
class TableViewCell: UITableViewCell {
var originalCenter = CGPoint()
var deleteOnDragRelease = false
let gradientLayer = CAGradientLayer()
required init(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// gradient layer for cell
gradientLayer.frame = bounds
let color1 = UIColor(white: 1.0, alpha: 0.2).CGColor as CGColorRef
let color2 = UIColor(white: 1.0, alpha: 0.1).CGColor as CGColorRef
let color3 = UIColor.clearColor().CGColor as CGColorRef
let color4 = UIColor(white: 0.0, alpha: 0.1).CGColor as CGColorRef
gradientLayer.colors = [color1, color2, color3, color4]
gradientLayer.locations = [0.0, 0.01, 0.95, 1.0]
layer.insertSublayer(gradientLayer, atIndex: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
// add a pan recognizer
var recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
recognizer.delegate = self
addGestureRecognizer(recognizer)
//MARK: - horizontal pan gesture methods
func handlePan(recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .Began {
// when the gesture begins, record the current center location
originalCenter = center
}
// 2
if recognizer.state == .Changed {
let translation = recognizer.translationInView(self)
center = CGPointMake(originalCenter.x + translation.x, originalCenter.y)
// has the user dragged the item far enough to initiate a delete/complete?
deleteOnDragRelease = frame.origin.x < -frame.size.width / 2.0
}
// 3
if recognizer.state == .Ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if !deleteOnDragRelease {
// if the item is not being deleted, snap back to the original location
UIView.animateWithDuration(0.2, animations: {self.frame = originalFrame})
}
}
}
}
// add a pan recogniser
From this point on in your code you are writing inside the body of the class rather than in a specific method.
The tutorial states
Open TableViewCell.swift and add the following code at the end of the overridden init method:
So you should probably do that. You've entered the code in the wrong place.

Resources