I can move a single UIView using the code below but how do I move multiple UIView's individually using IBOutletCollection and tag values?
class TeamSelection: UIViewController {
var location = CGPoint(x: 0, y: 0)
#IBOutlet weak var ViewTest: UIView! // move a single image
#IBOutlet var Player: [UIView]! // collection to enable different images with only one outlet
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first! as UITouch
location = touch.location(in: self.view)
ViewTest.center = location
}
}
There are two basic approaches:
You could iterate through your subviews figuring out in which one the touch intersected and move it. But this approach (nor the use of cryptic tag numeric values to identify the view) is generally not the preferred method.
Personally, I'd put the drag logic in the subview itself:
class CustomView: UIView { // or subclass `UIImageView`, as needed
private var originalCenter: CGPoint?
private var dragStart: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
originalCenter = center
dragStart = touches.first!.location(in: superview)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
var location = touch.location(in: superview)
if let predicted = event?.predictedTouches(for: touch)?.last {
location = predicted.location(in: superview)
}
center = dragStart! + location - originalCenter!
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: superview)
center = dragStart! + location - originalCenter!
}
}
extension CGPoint {
static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
}
Remember to set "user interaction enabled" for the subviews if you use this approach.
By the way, if you're dragging views like this, make sure you don't have constraints on those views or else when the auto-layout engine next applies itself, everything will move back to the original location. If using auto-layout, you'd generally modify the constant of the constraint.
A couple of observations on that dragging logic:
You might want to use predictive touches, like above, to reduce lagginess of the drag.
Rather than moving the center to the location(in:) of the touch, I would rather keep track of by how much I dragged it and either move the center accordingly or apply a corresponding translation. It's a nicer UX, IMHO, because if you grab the the corner, it lets you drag by the corner rather than having it jump the center of the view to where the touch was on screen.
I'd create a subclass of UIView which supports dragging. Something like:
class DraggableView: UIView {
func setDragGesture() {
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DraggableView.handlePanGesture(_:)))
addGestureRecognizer(panRecognizer)
}
func handlePanGesture(_ recognizer: UIPanGestureRecognizer) {
guard let parentView = self.superview else { return }
let translation = recognizer.translation(in: parentView)
recognizer.view?.center = CGPoint(x: recognizer.view!.center.x + translation.x, y: recognizer.view!.center.y + translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
}
func getLocation() -> CGPoint {
return UIView().convert(center, to: self.superview)
}
}
So then you can add an array of draggable views and then ask for the location when you need to finish displaying that view controller.
Related
I built a SplitView class that looks like this picture below:
As you can see the SplitView always have two subviews, therefore it has two properties that are leftView and rightView.
The job of the SplitView is to manage its subview size proportion.
If I do a swipe gesture to the left, it will move the separator location and it changes the size of each subviews, so it will look like this:
It works perfectly until I use UITableView as the leftView and rightView.
This is because the UITableView inside it is the one which will process the touch event, not the superview (which is SplitView)
And because the code to intercept and respond to the touch is in SplitView, it doesn't do anything if the UITableView inside it is the one that receives the touch event.
To do this I implement hitTest to make the SplitView the responder, not the UITableView inside it.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
Now I can get the touch event in the SplitView even though the user swipe on the UITableView.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
self.startInfo = StartInfo(timestamp: touch.timestamp,
touchLocation: touch.location(in: self),
separatorLocation: separatorView.frame.origin)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let startInfo = startInfo {
let location = touch.location(in: self)
if moveHorizontally == nil {
let deltaX = abs(location.x - startInfo.touchLocation.x)
let deltaY = abs(location.y - startInfo.touchLocation.y)
if deltaX > 4.0 || deltaY > 4.0 {
moveHorizontally = deltaX > deltaY
}
}
if let moveHorizontally = moveHorizontally {
if moveHorizontally {
print("the user intends to adjust separator position")
adjustSeparatorViewPosition(usingTouchLocation: location)
} else {
print("the user intends to scroll the table view")
if rightView.frame.contains(location) {
rightView.touchesMoved(touches, with: event) // doesn't work
} else {
leftView.touchesMoved(touches, with: event) // doesn't work
}
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startInfo = nil
moveHorizontally = nil
leftView.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
if touch.location(in: self).x < self.bounds.width / 2 {
UIView.animate(withDuration: 0.3) {
self.setSeparatorLocationX(to: self.separatorMinX)
}
} else {
UIView.animate(withDuration: 0.3) {
self.setSeparatorLocationX(to: self.separatorMaxX)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
startInfo = nil
moveHorizontally = nil
leftView.touchesCancelled(touches, with: event)
}
As you can see in this code, I added a delay, just like UIScrollView does when swiping to get the user intention, whether the user want to scroll horizontally or vertically.
- If the swipe direction is horizontal, I want to adjust the separator location
- If the swipe direction is vertical, I want to forward the event to the UITableView inside it (for example the leftView)
But, forwarding the touch event using rightView.touchesMoved(touches, with: event) doesn't work.
How to forward the touch event from the SplitView to the UITableView inside it?
I have a UILabel object which is being animated, moving up and down. I am coding in Xcode 8.3 with Swift 3. A user can pan this object and drag around to one location to get some points. I am handling the pan/drag using touchesXXX gesture. When I tap and let it go immediately, however, this object jumps vertically above its location for some reason and I am unable to figure this out why for a week now...
When I enabled some debugging, I can only see touchesBegan was invoked (touchesMoved was not called as expected, neither touchesEnded which appears unexpected to me). If I disable animation on the object manually, it works as expected and the object is able to be panned effortlessly and there is obviously no object jumps.
Here is an extract of the relevant code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
//pauseLayer(self.optOneLbl.layer) <<<<< comment #1
//optOneLbl.translatesAutoresizingMaskIntoConstraints = true <<<<< comment #2
optOneLbl.center = touchLocation
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
var sender: UILabel
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
self.optOneLbl.center = touchLocation
sender = self.optOneLbl
}
// identify which letter was overlapped
var overlappedView : UILabel
if (sender.frame.contains(letter1.center)) {
...
}
else {
return
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
}
After reading responses to other SO questions, I thought disabling the label animation programmatically when touched as per above comment #1 in touchesBegan might help, but the issue persisted. Also, I thought may be Auto Layout is causing this weird jump. So, I enabled translatesAutoresizingMaskIntoConstraints as per comment #2, but it too didn't help.
Anyone is able to see where I am handling this incorrectly?
Thank you for reading!
EDIT:
As per #agibson007 request, I am adding the animation code extract for reference:
UIView.animate(withDuration: 12.0, delay: 0.0, options: [ .allowUserInteraction, .curveLinear, .autoreverse, .repeat ], animations: {
self.optOneLbl.center = CGPoint(x: self.optOneLbl.center.x, y: screenSize.midY*1.1)
})
You need to remove/reset the animation after you change the location of the label. The animation does not know you updated the values and is trying to stay in the same range of byValue from the beginning. Here is a working example of updating the animation.
import UIKit
class ViewController: UIViewController {
var optOneLbl = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
optOneLbl = UILabel(frame: CGRect(x: 20, y: 50, width: self.view.bounds.width - 40, height: 40))
optOneLbl.textAlignment = .center
optOneLbl.text = "I Will Be Moving Up and Down"
optOneLbl.textColor = .white
optOneLbl.backgroundColor = .blue
self.view.addSubview(optOneLbl)
//starts it in the beginnning with a y
fireAnimation(toY: self.view.bounds.midY*1.1)
}
func fireAnimation(toY:CGFloat) {
UIView.animate(withDuration: 12.0, delay: 0.0, options: [ .allowUserInteraction, .curveLinear, .autoreverse, .repeat ], animations: {
self.optOneLbl.center = CGPoint(x: self.optOneLbl.center.x, y:toY)
})
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
//re
optOneLbl.layer.removeAllAnimations()
optOneLbl.center = touchLocation
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
var sender: UILabel
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
self.optOneLbl.center = touchLocation
sender = self.optOneLbl
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
//restart animation after finished and change the Y if you want.
// you could change duration or whatever
fireAnimation(toY: self.view.bounds.height - optOneLbl.bounds.height)
}
}
You can achieve the same using UIPanGestureRecognizer. Add pan gesture to your label and move the label on pan as
#IBAction func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.view)
yourLabel!.center = CGPoint(x: yourLabel!.center.x + translation.x, y: yourLabel!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
}
}
You can add gesturerecognizer to your UILabel from storyboard or programmatically in viewDidLoad as
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
yourLabel.addGestureRecognizer(gestureRecognizer)
Using Pan gesture as advantage that you dont have to follow up touchesXX method. To why your view is moving up, I think you might be resetting your label frame some where else too. There are also chances for gesture conflicting with touches. As both the cases are not evident in your code provided we need more code to confirm the same.
So I found out how to make a button draggable using the UIPanGestureRecognizer. But the only way I know how to do it is by storing and dragging the button by the center. The problem with this is if you try and drag the button from a corner, the button instantly shifts from the corner to the center. What I'm looking for is a solution that would keep my finger on a selected place while moving without instantly locking onto the center.
The code I'm currently using:
func buttonDrag(pan: UIPanGestureRecognizer) {
print("Being Dragged")
if pan.state == .began {
print("panIF")
buttonCenter = button.center // store old button center
}else {
print("panELSE")
let location = pan.location(in: view) // get pan location
button.center = location // set button to where finger is
}
}
Thanks in advance.
This can be done at least in two different ways, one using GestureRecognizer your question way and other way is subclassing the UIView and implementing the touchesBegan, touchesMoved , touchesEnded, touchesCancelled in general will work for any UIView subclass can be UIButton or UILabel or UIImageView etc...
In your way, using GestureRecognizer I make a few changes you still require a var to keep the origin CGPoint of the touch in your UIButton so we get the touch position relative to the UIButton and when the drag continue adjust the UIButton origin according to the origin touch position and the positions of the movement
Method 1 GestureRecognizer
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
var buttonOrigin : CGPoint = CGPoint(x: 0, y: 0)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let gesture = UIPanGestureRecognizer(target: self, action: #selector(buttonDrag(pan:)))
self.button.addGestureRecognizer(gesture)
}
func buttonDrag(pan: UIPanGestureRecognizer) {
print("Being Dragged")
if pan.state == .began {
print("panIF")
buttonOrigin = pan.location(in: button)
}else {
print("panELSE")
let location = pan.location(in: view) // get pan location
button.frame.origin = CGPoint(x: location.x - buttonOrigin.x, y: location.y - buttonOrigin.y)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Method 2 UIView subclass in this case UIButton subclass
Use this UIButton subclass
import UIKit
class DraggableButton: UIButton {
var localTouchPosition : CGPoint?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch = touches.first
self.localTouchPosition = touch?.preciseLocation(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let touch = touches.first
guard let location = touch?.location(in: self.superview), let localTouchPosition = self.localTouchPosition else{
return
}
self.frame.origin = CGPoint(x: location.x - localTouchPosition.x, y: location.y - localTouchPosition.y)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
self.localTouchPosition = nil
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
self.localTouchPosition = nil
}
}
Result
Hope this helps you
In my main view in a UIViewController I have a mapView and a another view (Let's say view A) that is above mapView. Both of them have frames equal to self.view.bounds. The view A is a rectangle that is resizable similar to those used to crop images. My goal here is to let the user specify an area on the map. So, I want the user to be able to zoom in an out of the map as well as change the rectangle width and height proportions since only letting the view A to be an unrealizable square would limit it too much.
I got this project from GitHub https://github.com/justwudi/WDImagePicker from which I am using the resizable rectangle functionality. In the second picture of the Github link, there's a rectangle with 8 dots and a shaded area outside. I want to be able to let the touch pass to the map which is behind the view A if the user touches on the shaded area. Only if the user clicks on the area inside the dots or on the dots (so that he wants to resize the rectangle) I want the view A to recognize the touch. So, I modified the code on touch on the view A and have this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if cropBorderView.frame.contains(touch.location(in: self)){
print("touch contains - touchesbegan")
//self.isUserInteractionEnabled = true
}
else{
print("Touch does not contain - touchesbegan")
self.touchesCancelled(touches, with: event)
//return
}
let touchPoint = touch.location(in: cropBorderView)
anchor = self.calculateAnchorBorder(touchPoint)
fillMultiplyer()
resizingEnabled = true
startPoint = touch.location(in: self.superview)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("inside touches moved")
if let touch = touches.first {
if cropBorderView.frame.contains(touch.location(in: self)){
print("touch contains - touchesmoved")
//self.isUserInteractionEnabled = true
}
else{
print("Touch does not contain - touchesmoved ")
self.touchesCancelled(touches, with: event)
//return
}
if resizingEnabled! {
self.resizeWithTouchPoint(touch.location(in: self.superview))
}
}
}
It is indeed recognizing the touch when I click inside and outside as I wanted, but it is not stopping the touch when I click outside. This means calling self.touchesCancelled(touches, with: event) is not working. Calling return gives a crash and does not work as well. Are there any solutions to this problem?
Thank you for your time and consideration.
touchesCancelled(_:with:) just a notification for UITouch, it will not work this way.
As far as I understand, you implemented touch handlers in your overlay UIView, if so, you can try to replace the call to self.touchesCancelled(touches, with: event) with cancelTracking(with:) function from UIControl class implementation:
else {
print("Touch does not contain - touchesmoved ")
self.cancelTracking(with event)
}
Update solution, based on hitTest:
I've checked possible solutions and it seems that you can use hitTest: to avoid unnecessary touch recognitions. The following example is Swift Playground, you can tap and drag touches and see what happens in the console:
import UIKit
import PlaygroundSupport
class JSGView : UIView {
var centerView = UIView()
override func didMoveToSuperview() {
frame = CGRect(x: 0, y: 0, width: 320, height: 480)
backgroundColor = UIColor.clear
centerView.frame = CGRect(x: 110, y: 190, width: 100, height: 100)
centerView.backgroundColor = UIColor.blue
addSubview(centerView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
dump(event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if (hitTest(touch.location(in: self), with: event) != nil) {
print("Touch passed hit test and seems valid")
super.touchesCancelled(touches, with: event)
return
}
}
print("Touch isn't passed hit test and will be ignored")
super.touchesMoved(touches, with: event)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if centerView.bounds.contains(centerView.convert(point, from: self)) {
return centerView
}
return nil
}
}
class JSGViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
let customView = JSGView()
view.addSubview(customView)
}
}
let controller = JSGViewController()
PlaygroundPage.current.liveView = controller.view
So I have created a button with a border in my storyboard.
and then I rounded its corners and added a border color:
button.layer.cornerRadius = button.bounds.size.width / 2
button.layer.borderColor = greenColor
So the runtime result looks like this:
However the user can tap slightly outside the area of the button (where the corners used to be) and still call the button function. Is there a way to restrict the enabled area of the button to just be the circle?
With other answers you block the touch, I needed it to fall through.
And its even easier:
1) Setup your preferred path (for me circle)
private var touchPath: UIBezierPath {return UIBezierPath(ovalIn: self.bounds)}
2) Override point inside function
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return touchPath.contains(point)
}
All inside you UIButton subclass.
So I figured it out. Basically I have to detect the touch on the button, and then calculate the distance between the touch and the center of the button. If that distance is less than the radius of the circle (the width of the button / 2) then the tap was inside the circle.
Here's my code:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let radius:CGFloat = (self.frame.width / 2)
var point:CGPoint = CGPoint()
if let touch = touches.first {
point = touch.locationInView(self.superview)
}
let distance:CGFloat = sqrt(CGFloat(powf((Float(self.center.x - point.x)), 2) + powf((Float(self.center.y - point.y)), 2)))
if(distance < radius) {
super.touchesBegan(touches, withEvent: event)
}
}
(SWIFT 3) This solution works with all the buttons not just with round. Before we have to create path as a private property of the button class and after we can easily write this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
if path.contains(location) {
print("This print is shown only in case of button location tap")
}
}
}