I am trying to change UIView frame on dragging. I subclassed UIView and wrote touchesBegan and touchesMoved methods. Here they are:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
startPosition = touch?.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let endPosition = touch?.locationInView(self)
let difference = endPosition!.y - startPosition.y
let newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.width, self.frame.height + difference)
self.frame = newFrame
}
But as a result I got unexpected behaviour. It is a little bit hard to describe it. Here's video how it works.. Small drag(about 20px) makes frame bigger on approximately 100px.
I also have some constraints on my custom view. Image at the bottom of custom view should stay always there:
So questions are:
How to make view bigger on distance user dragged.
And why constraints do not apply after changing frame and how to apply them again?
The reason your view is growing quickly is that you are always updating the current view height with the distance you have moved since your first touch down. Instead of modifying the current height, you should always add the distance moved to the original height of the view.
Add another property to your custom view to keep track of the original height of the view. Set it in touchesBegan and then use that originalHeight when computing the frame in touchesMoved:
class CustomView: UIView {
var startPosition: CGPoint?
var originalHeight: CGFloat = 0
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
startPosition = touch?.locationInView(self)
originalHeight = self.frame.height
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let endPosition = touch?.locationInView(self)
let difference = endPosition!.y - startPosition!.y
let newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.width, originalHeight + difference)
self.frame = newFrame
}
}
And why constraints do not apply after changing frame and how to apply
them again?
You really shouldn't be updating frames when using Auto Layout. The correct way to do this is to add an #IBOutlet to the height constraint for your view and then update the constant property of the constraint when you wish to change the height of your view.
When you do that, Auto Layout will correctly position the subviews of your custom view using the updated height.
To add an #IBOutlet to the height constraint to your code, Control-drag from the height constraint in the Document Outline to your viewController. Give it a name such as customViewHeight.
Since your code for updating the height of the view is in the view code itself, have the viewController assign the height constraint to the custom view in viewDidLoad. Then the custom view can update the constant property of the height constraint to resize the view.
class ViewController: UIViewController {
#IBOutlet weak var customViewHeight: NSLayoutConstraint!
#IBOutlet weak var customView: CustomView!
override func viewDidLoad() {
super.viewDidLoad()
// Assign the layout constraint to the custom view so that it can update it itself
customView.customViewHeight = customViewHeight
}
}
class CustomView: UIView {
var startPosition: CGPoint?
var originalHeight: CGFloat = 0
var customViewHeight: NSLayoutConstraint!
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
startPosition = touch?.locationInView(self)
originalHeight = customViewHeight.constant
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let endPosition = touch?.locationInView(self)
let difference = endPosition!.y - startPosition!.y
customViewHeight.constant = originalHeight + difference
}
}
In your case, I think you should change the constant of the bottom constraints instead of changing the frame of the view. And by the way, you'd better use gestures instead of overriding touch methods since gestures are higher level API and easier to maintain
Basically, projects using Autolayout should avoid changing views with frame.
Related
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.
I have a scrollView with a subview inside of it. I want any touch outside of the subview to make the scrollview scroll like it would in any other circumstance. But then when the touch is inside the subview I'd like no scroll to be registered (so I can use that touch to do dragging).
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.backgroundColor = UIColor.red
scrollView.contentSize = CGSize(width: 1000, height: 1000)
let view = UIView(frame: CGRect(x: 600, y: 600, width: 200, height: 200))
view.backgroundColor = UIColor.blue
scrollView.addSubview(view)
}
}
So far I have tried many things to make this work. I have had some success with making customs classes for both the subview and the scrollview. In these classes I override the touch methods like so:
class MyScrollView: UIScrollView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isScrollEnabled = true
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isScrollEnabled = true
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isScrollEnabled = true
}
}
For the scrollview customclass I turned scrolling on and then for the subview custom class I turned it off. This works but there is a major problem that the first touch still goes ahead before the isScrollEnabled changes. This means when the scroll is off and you want to scroll you have to touch outside of the box and nothing happens and then do the same touch and the touch begins.
What I am looking for is a way to realise where a touch is, change the scroll settings and then on that same touch still scroll or not scroll dependent on what the setting is.
Cheers, any help is appreciated.
Probably the easiest way to do it is to add a dummy UIPanGestureRecognizer to the blue view like this:
blueView.addGestureRecognizer(UIPanGestureRecognizer())
Even though there is no action, this will do the job because a touch will be handled by the subview first and only then handled by UIScrollView's own UIPagGestureRecognizer.
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")
}
}
}
I'm new in Swift and I'm trying to let the user draw a rectangle (touching and dragging) to select an area of an image just like when cropping but I don't want to crop I just want to know the CGRect the user created.
So far I have a .xib with a UIImage inside and its ViewController. I want to draw above the image but every tutorial I found about drawing is about subclassing UIView, override drawRect and put that as the xib class.
I figured it out. I just created a uiview and change its frame depending on the touches events
let overlay = UIView()
var lastPoint = CGPointZero
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
overlay.layer.borderColor = UIColor.blackColor().CGColor
overlay.backgroundColor = UIColor.clearColor().colorWithAlphaComponent(0.5)
overlay.hidden = true
self.view.addSubview(overlay)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Save original tap Point
if let touch = touches.first {
lastPoint = touch.locationInView(self.view)
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Get the current known point and redraw
if let touch = touches.first {
let currentPoint = touch.locationInView(view)
reDrawSelectionArea(lastPoint, toPoint: currentPoint)
}
}
func reDrawSelectionArea(fromPoint: CGPoint, toPoint: CGPoint) {
overlay.hidden = false
//Calculate rect from the original point and last known point
let rect = CGRectMake(min(fromPoint.x, toPoint.x),
min(fromPoint.y, toPoint.y),
fabs(fromPoint.x - toPoint.x),
fabs(fromPoint.y - toPoint.y));
overlay.frame = rect
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
overlay.hidden = true
//User has lift his finger, use the rect
applyFilterToSelectedArea(overlay.frame)
overlay.frame = CGRectZero //reset overlay for next tap
}
I want show the x corrdinate of a touched and moved object.
Try it out:
import UIKit
class ViewController: UIViewController {
var location = CGPoint(x: 0, y: 0)
#IBOutlet weak var viewBox: UIView!
#IBOutlet weak var cntInViewBox: UILabel!
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first! as UITouch
location = touch.locationInView(self.view)
viewBox.center = location
/*cntInViewBox.text=String(location.x)*/
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first! as UITouch
location = touch.locationInView(self.view)
viewBox.center = location
/*cntInViewBox.text=String(location.x)*/
}
override func viewDidLoad() {
super.viewDidLoad()
viewBox.center = CGPoint(x:0, y: 0)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Without the Line
cntIntViewBox.text=String(location.x)
it works great. Touching on the sceen, the viewBox jumps, starting Touching, the box is moving.
But if I activate the line to show the coordinate (for example, I mean any dynamic text), moving and jumping is not working anymore.
why does this issue arise?
The problem is that your line changes the text of a label. This causes Auto Layout to be performed throughout your interface. Your viewBox view is positioned by Auto Layout constraints, so those constraints take over and are used to position the view. The constraints didn't change so the view doesn't move.