Drag UIImageView and Drop in another UIImageView - ios

I have a UIScrollView that contains n number of placeholder UIImageView's. Below that i have a StackView containing 10 images that the user will choose from and drag an image to a specific placeholder image view above. I have the PanGesture implemented and that works fine, however, i'm struggling with how to detect when the draggable image is within the bounds of a placeholder image. Especially since it is in a scroll view. I was able to get it to some what work with the center.X and center.Y of the draggable views then checking those values against their relative values of the placeholders, but it did not work as desired. And from what i can see, the X and Y coordinates from dragging, only apply to the actual parent UIView. So, when an image is dragged into the scroll view, the X values don't necessarily line up, some of those placeholder image X coordinates may be 2000+ if the list is long. Take a look at the code below and let me know your thoughts and suggestions. Thanks!
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
if recognizer.state == UIGestureRecognizerState.Began {
originalViewLocation = recognizer.view!.center
}else if recognizer.state == UIGestureRecognizerState.Ended {
for placeHolderView in playerImages{
if ((recognizer.view!.center.x < (placeHolderView.center.x + (placeHolderView.frame.width / 2)) && recognizer.view!.center.x > placeHolderView.frame.origin.x)){
if recognizer.view!.center.y < (stackView.center.y + stackView.frame.height / 2){
let chosenImage = recognizer.view! as! UIImageView
placeHolderView.image = chosenImage.image
}
}else{
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseInOut, animations: {
recognizer.view!.center = self.originalViewLocation
}, completion: { finished in
})
}
}
}
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: self.view)
print("X: \(recognizer.view!.center.x)")
print("Y: \(recognizer.view!.center.y)")
}
The playerImages array stores the UIImageViews in the scroll view.

Well i did solve this on my own. It had to do with the relative coordinate values vs actual coordinate values.
I ended up needing a few things...
CGRectContainsPoint()
To check whether or not the frame of the destination placeholder view contains the center point of the dragging view.
And...
convertRect()
with
convertPoint()
I had to convert the coordinate values of the dragging point to the values of the superview. Along with, converting the frame values of the placeholder views within the scroll view, to the same (superview values). Like so...
let convertedCenter = recognizer.view!.superview!.convertPoint(recognizer.view!.center, toView: self.view)
let convertedFrame = self.view.convertRect(IV.frame,fromView: IV.superview)
if CGRectContainsPoint(convertedFrame, convertedCenter) {
//Replace placeholder image with dragged image.
}
Hopefully this will help someone else.

Related

How can text new line when scale textview?

I am working on TextView as like Sticker View.I am successfully done but I can not wrap as I want. I want like this please check this clip and I am getting this output please check this output clip. From 1st clip I want like that and second video I am getting that.
In 1st video you can see when scaling the TextView It doesn't scale with center and when panning from right side of the TextView you can see it will decrease/increase width only.
In 2nd video(This is I am getting) you can see when scaling the TextView it will scale from the center that I don't want.
I want scale like 1st video. Hope you are understand.
I'm using this code
#objc func scaleGesture(_ recognizer: UIPanGestureRecognizer) {
touchLocation = recognizer.location(in: superview)
if recognizer.state == .began {
initialBounds = bounds
}
if recognizer.state == .changed {
let horizontalChange = recognizer.translation(in: self).x
let verticalChange = recognizer.translation(in: self).y
bounds = CGRect(x: initialBounds!.origin.x, y: initialBounds!.origin.y, width: initialBounds!.width + horizontalChange, height: initialBounds!.height + verticalChange)
}
}
Please help me. Thanks in advance.

Panning UIViewController makes things pinned to safe area jump to the superview

I'm using this code to make a UIViewController pannable take from this so post.
class ViewControllerPannable: UIViewController {
var panGestureRecognizer: UIPanGestureRecognizer?
var originalPosition: CGPoint?
var currentPositionTouched: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
view.addGestureRecognizer(panGestureRecognizer!)
}
func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
if panGesture.state == .began {
originalPosition = view.center
currentPositionTouched = panGesture.location(in: view)
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
} else if panGesture.state == .ended {
let velocity = panGesture.velocity(in: view)
if velocity.y >= 1500 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.origin.x,
y: self.view.frame.size.height
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center = self.originalPosition!
})
}
}
}
}
This works great on older phones that don't have a notch. But if the phone has a notch, once you start panning, any views pinned to the safe area jump to the superview. I think the issue is in the
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
But I'm not sure how to make anything that was pinned to the safe area stay that way when panning.
You want to avoid layoutSubviews after you drag the view outside the superview's frame. As long as you are not calling setNeedsLayout manually, you can achieve this by ensuring that last frame.size change happens before crossing outside. Just make enough variables to precisely control for this situation.
I encountered this issue with a custom action-sheet-like presentation. When I pulled the view down to dismiss, there was no problem. But when I pulled the view up to stretch and then down, the safeAreaInsets jumped from 0 to non-zero suddenly.
The reason was because my view stretched upwards but not downwards. Pull up was frame.origin.y change and a frame.size.height change. Downwards was only a frame.origin.y change. However, UIPanGestureRecognizer does not have infinite granularity, and so the jump from negative to positive translation caused origin.y to change at the same time as frame.size.height in the downward direction. This triggered layoutSubviews while the view was partially outside the superview, which meant it ran layout code that suddenly acknowledged a safeAreaInsets change.
My solution is essentially:
if translation < 0 && oldTranslation > 0 || translation > 0 && oldTranslation < 0 {
view.frame = originalFrame
} else {
view.frame = calculate(translation)
}
This forces the view to be a static size before it crosses the superview's boundary, ensuring that layoutSubviews is called with the correct safeAreaInsets, but as the position changes, the safeArea does not.
Your situation may be a little different if you have something else triggering layoutSubviews. You might have to hunt that down.
Another thing I notice is that you're moving the UIViewController's view when it might have a parent, which could have other implications or edge cases because safeArea is inherited from the UIViewController hierarchy rather than solely superview.

How to mimic Apple Map's translating table view?

I am making an app that returns venues (like restaurants) in a fashion similar to apple maps, i.e. a table view that is initially hidden mostly off screen and then moves up above other content when the table view's contents are to be displayed.
This is what I've gotten so far:
You'll notice that when pulling the table view back down to reveal the map behind it, the table view does not move down in one fluid motion. In fact, when the table view reaches its "top," you'll notice that the scroll bar on the right side is visible but the view isn't moving. This is because, while recording, I'm dragging downward but the view takes a few moments to actually begin to translate downward. I'll explain why it has this sort of delay, but first compare this behavior to that of the Apple maps table view:
Notice the fluidity in the Apple maps table view's transition from translating the view itself to scrolling the scroll view. A translation upward of the view converts into a scrolling downward of the scroll view (and vice versa) when the view's minY hits a certain point. This is all done throughout one upward swipe by the user.
Now I'll explain what I did to create my translating scroll view. The scroll view itself is a UITableViewController embedded in a container view on the map's view controller.
The view that holds the container view has a pan gesture recognizer. When the pan gesture recognizer's state is UIGestureRecognizerState.ended, i.e. the drag has ended, it checks the container view's minY in relation to a predetermined Y coordinate. If the minY is less than the predetermined Y coordinate, it will animate the container view to snap its minY to that Y coordinate. The case is the same if the minY is slightly greater than the predetermined Y coordinate, in this instance the container view will animate upward so its minY snaps to this predetermined Y coordinate.
Once the view has snapped into this position, the embedded UITableViewController's ".isScrollEnabled" property, which is initially set to false, is now set to true. When the embedded table view is now scroll enabled, a drag executed by the user will scroll the table view as opposed to translating the container view. To undo this, I've made it so that when the table view has reached the top of its scroll, its ".isScrollEnabled" property is set back to false. This means that any dragging motion by the user will translate the view as opposed to scrolling the table view. This does not create the fluidity in transition between scrolling and translating the way Apple maps does. The user would actually have to execute multiple drags on the area in order to transition from scrolling to translating; Apple maps is able to do this in one drag.
Here is the code I used within the container view's parent View controller to:
Snap container view into place
Change .isScrollEnabled property of embedded table view controller to true
#objc func handlePan(sender: UIPanGestureRecognizer) {
let resultView = self.resultView
let translation = sender.translation(in: view)
switch sender.state {
case .began, .changed:
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! + translation.y)
sender.setTranslation(CGPoint.zero, in: view)
if (resultView?.frame.minY)! <= self.view.frame.height - self.searchView.frame.height - self.view.safeAreaInsets.bottom && self.resultView.frame.height >= (self.searchView.frame.height + self.view.safeAreaInsets.bottom) {
resultView?.frame.size.height -= translation.y
}
case .ended:
if (resultView?.frame.minY)! > self.view.frame.height * 0.40 {
self.resultTableViewController?.tableView.isScrollEnabled = false
} else {
self.resultTableViewController?.tableView.isScrollEnabled = true
}
if (resultView?.frame.minY)! <= view.frame.height * 0.40 {
let difference = (resultView?.frame.minY)! - (view.frame.height * 0.05)
UIView.animate(withDuration: 0.25) {
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! - difference)
resultView?.frame.size.height += difference
self.view.layoutIfNeeded()
}
dismissed = false
} else if (resultView?.frame.minY)! >= view.frame.height * 0.75 {
let difference = (view.frame.height - dismissResultButton.frame.height - view.safeAreaInsets.bottom) - (resultView?.frame.minY)!
UIView.animate(withDuration: 0.25, animations: {
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! + difference)
self.resultView.frame.size.height = self.searchView.frame.size.height + self.view.safeAreaInsets.bottom
self.view.layoutIfNeeded()
})
dismissed = true
} else if (resultView?.frame.minY)! >= view.frame.height * 0.40 {
let difference = mapView.frame.height - (resultView?.frame.minY)!
UIView.animate(withDuration: 0.25) {
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! + difference)
self.resultView.frame.size.height = self.searchView.frame.size.height + self.view.safeAreaInsets.bottom
self.view.layoutIfNeeded()
}
dismissed = false
}
sender.setTranslation(CGPoint.zero, in: view)
default:
break
}
}
And now, within the UITableViewController:
Ensures that the scroll view only bounces when at the bottom of the scroll (it should lock and then convert to a container translation when reaching the top)
Change .isScrollEnabled back to false and allow container view translation on drag
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 0)
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview!)
if translation.y > 0 {
if scrollView.contentOffset.y == 0 {
tableView.isScrollEnabled = false
}
}
}
This is the best I could come up with to try and replicate this behavior by the Apple maps table view. Any help/suggestions would be appreciated.
P.S. heres the github to the project if you'd like to break it down more: https://github.com/ChopinDavid/What2Do

How to limit the area in which an object can be moved to the boundaries of another view with pan gestures?

I want to restrict the area a UITextView can be moved to only the frame of another view. I tried to accomplish this with the following code, however, it will not properly restrict where the user can drag the UITextView:
#IBOutlet weak var canvas: UIView!
#IBAction func handleDragOfCaption(_ sender: UIPanGestureRecognizer) {
if sender.state == .began || sender.state == .changed {
guard let senderView = sender.view else {return}
let translation = sender.translation(in: sender.view)
let x = senderView.center.x + translation.x
let y = senderView.center.y + translation.y
let point = CGPoint(x: x, y: y)
if canvas.frame.contains(point) {
senderView.center = point
sender.setTranslation(CGPoint.zero, in: senderView)
}
}
}
See the following image for a visual representation of what I am trying to accomplish. I only want the user to be able to drag the UITextView within the boundaries of the UIView (which is the white square underneath the UITextView). Any help would be appreciated.
Here is another approach, using constraints...
Give your "draggable view" (your text view, in this case) a Width constraint and disable scrolling (that will allow it to auto-size its height).
Constrain the "draggable view" to Zero on all four sides - then edit those constraints to be >= 0. This will prevent the view from being dragged outside its parent view.
Also constrain the "draggable view" to Center Horizontally and Vertically - then edit those constraints to have Priority = 750. By setting the center constraints to a lower priority than the edge constraints, the centers will only take effect if the edges are fully inside the parent view.
Connect the center constraints to IBOutlets in your view controller, and add vars for "current" constant values for the center constraints.
Now add a Pan Gesture Recognizer to the "draggable view".
When you start the pan, save the center constraint constants to the "current" vars.
When you pan, get the translation X and Y (the distance moved from the start of the pan), and update the center constants based on the difference between the start and new values. This will move the "draggable view" to its new position.
When you finish the pan, update the center constants based on the final position of the "draggable view".
#IBAction func handleDragOfCaption(_ sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
guard let parentView = senderView.superview else { return }
// get the current pan movement - relative to the view (the text view, in this case)
let translation = sender.translation(in: canvas)
switch sender.state {
case .began:
// save the current Center X and Y constants
currentCenterXConstant = textViewCenterXConstraint.constant
currentCenterYConstant = textViewCenterYConstraint.constant
break
case .changed:
// update the Center X and Y constants
textViewCenterXConstraint.constant = currentCenterXConstant + translation.x
textViewCenterYConstraint.constant = currentCenterYConstant + translation.y
break
case .ended, .cancelled:
// update the Center X and Y constants based on the final position of the dragged view
textViewCenterXConstraint.constant = senderView.center.x - parentView.frame.size.width / 2.0
textViewCenterYConstraint.constant = senderView.center.y - parentView.frame.size.height / 2.0
break
default:
break
}
}
To help with setting up the constraints correctly, I've posted an example project here: https://github.com/DonMag/LimitDrag

Dragging UIView loses the gesture when ended

I have the following code that drags a UIView. All works fine visually.
func moveView(sender: UIPanGestureRecognizer) {
let translate = sender.translationInView(self.view)
if sender.state == UIGestureRecognizerState.Changed {
sender.view!.center = CGPoint(x:sender.view!.center.x + translate.x, y:sender.view!.center.y + translate.y)
sender.setTranslation(CGPointZero, inView: self.view)
}
if sender.state == UIGestureRecognizerState.Ended {
let newX: CGFloat = sender.view!.center.x + translate.x
let newY: CGFloat = sender.view!.center.y + translate.y
sender.view!.center = CGPoint(x:newX, y:newY)
}
}
However after completing this drag, the view seems to lose the gesture connection such that I can't drag it again or trigger any tap gesture associated with it etc.
If I add an NSLog I can see that tapping where the view used to be triggers the log but not if I tap on the actual current view location.
I establish the gesture to view thisView with the following within viewDidLoad
let moveGesture = UIPanGestureRecognizer(target: self, action: Selector("moveView:"))
thisView.addGestureRecognizer(moveGesture)
What am I missing that keeps the gestures connected to the new view location?
Thanks.
I think that when you call this sender.setTranslation(CGPointZero, inView: self.view) maybe you should set the translation relative to superview and not to the view itself. Setting the translation relative to itself change the position of the view contents but not the view area, which means that if you set the view layer to mask to its bounds you shouldn't see anything that it's outside of the view initial area.
So you should do sender.setTranslation(CGPointZero, inView: self.view.superview).

Resources