Use UIPanGestureRecognizer to drag view from one position to limited position - ios

In my application I have an UIView.I want functionality such that user can drag the view from its original position to particular limited position for this I have used **UIPanGestureRecognizer Class ** and in gestureRecognizer.state == .Changed condition I am changing the coordinates of view .I am able to drag the view to limited position when moving slowly but The problem is if the user drags the view very rapidly upward or downward the screen, then the view can be pulled beyond the limits I put on the Y position
if(upperLimit > (self.topbaseConstrant.constant * -1))
{
self.topbaseConstrant.constant += gestureRecognizer.translationInView(self.view!).y
gestureRecognizer.setTranslation(CGPointZero, inView: self.view!)
}
I have been trying to solve the issue since last three days .Please give me suggestion
Thanks in advance

Use the min function to determine upper limits
let newPosition = topbaseConstrant.constant + panGestureRecognizer.translationInView(nil).y
topbaseConstrant.constant = min(upperLimit, newPosition)
If you drag quickly and blow past your constraint, the min function will always return that upper constraint as your new position.

Related

Which SwiftUI node is *currently* being touched?

TLDR; If the user drags their finger over multiple nodes, how can I get all the nodes they touched while dragging?
I'm trying to build a Wordsearch-like game to teach myself SwiftUI on iOS.
I've built a grid of tiles as shown (ignore blue colour tiles as they are not relevant for my question).
I'd like the user to drag their finger over the grid, and for every tile they swipe over, I'll call a method on my model to mark that tile as "selected".
I've tried a DragGesture on each tile's view, like so;
.gesture(
DragGesture()
.onChanged { _ in
myModel.SelectTile(self.tile.id);
}
)
However as the user's finger moves out of the starting tile and into the next, the next node's drag gesture is not actioned until the user lifts their finger and starts a new gesture on that tile.
Is this behaviour possible with SwiftUI? I realise I can probably do this more easily with SpriteKit but that seems a little overkill for this kind of "game".
I've found another way to accomplish what I need;
By applying the DragGesture to the grid itself; I can get the touch location within the grid.
Therefore a bit of simple division and flooring gives me the location of the touched tile in the grid, like so;
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _gesture in
let tileSize = geometry.size.width / CGFloat(self.game.grid.width);
let x : Int = Int(_gesture.location.x / tileSize);
let y : Int = Int(_gesture.location.y / tileSize);
self.game.SelectTile(_x: x, _y: y);
}
)

How to calculate a random CGPoint which does not touch a UIView

This is an example view:
I want to calculate a frame with a CGPoint where I can spawn another card(UIView) without touching any existing card. Ofcourse it is optional since the view can be full of cards, therefore there is no free spot.
This is how I can see any card on the screen and my function how it is now:
func freeSpotCalculator() -> CGPoint?{
var takenSpots = [CGPoint]()
for card in playableCards{
takenSpots.append(card.center)
}
}
I have no idea where to start and how to calculate a random CGPoint on the screen. The random frame has the same width and height as a card in on the screen.
The naive approach to this is very simple, but could be problematic once the screen fills up. Generate a random CGPoint with x coordinate between 0 and the screen width and a y coordinate between 0 and the screen height. Check if a rectangle with a center at that point intersects any existing view. If it does not, you have your random position.
Where this gets problematic is when the screen starts to fill up. At that point you could be trying many many random points before finding a place to put the card. You could also reach a situation where no more cards will fit. How do you know that you have reached that? Will your loop generating the random points just run forever?
A smarter solution is to keep track of the free spaces on the screen. Always generate your random points roughly within these free spaces. You could do this using a grid if approximate is close enough. Is there a card occupying each grid location? Then when the largest free space is smaller than the size of your card rectangle, you know you're done. It's a lot more work than the naive approach, but it's faster when the screen starts to fill up and you'll know for sure when you're done.
If you know that you will always have more screen space than the cards can possibly take up, the naive approach is fine.
The idea
You know the width and height of your container UIView. And, each card has the same width and height. I would go about this by calculating a grid.
Even though you want to display cards randomly, relying on a grid will give you a standardized array of centers that you can use to generate the appearance of randomness (place a card at any random center that is a part of the grid, for example).
If you were to place a card at truly any random location, you might just want to use CGRectIntersectsRect(card1.frame, card2.frame) to detect collisions.
The pattern
First, let's store the card width and height as constants.
let cardWidth = card.bounds.size.width
let cardHeight = card.bounds.size.height
As a basic proof of concept, let's say your container view width is 250 points. Let's say the card width is 5 points. That means you can fit 250 / 5 = 50 cards in one row, where one row has the height of one card.
The number of centers in a row = the number of cards in that row. Each center is the same distance apart. In the following diagram (if I can even call it that), the [ and ] represent edges of a row. The -|- represents a card, where | is the center of the card.
[ - | - - | - - | - - | - - | - ]
Notice how every center is two dashes away from the next center. The only consideration is that the center next to the edge is one dash away from the edge. In terms of cards, each center is one whole card away from the next, and the centers next to the edges are one half card away from the edges.
The key to the pattern
This pattern means that the x position of any card center in a specific row = (cardWidth / 2) + (the card index * cardWidth). In fact, this pseudo-equation works for y positions as well.
The code
Here's some Swift that creates an array of centers using this method.
var centers = [CGPoint]()
let numberOfRows: CGFloat = containerView.bounds.size.height / cardHeight
let numberOfCardsPerRow: CGFloat = containerView.bounds.size.width / cardWidth
for row in 0 ..< Int(numberOfRows) {
for card in 0 ..< Int(numberOfCardsPerRow) {
// The row we are on affects the y values of all the centers
let yBasedOnRow = (cardHeight / 2) + (CGFloat(row) * cardHeight)
// The xBasedOnCard formula is effectively the same as the yBasedOnRow one
let xBasedOnCard = (cardWidth / 2) + (CGFloat(card) * cardWidth)
// Each possible center for this row gets appended to the centers array
centers.append(CGPoint(x: xBasedOnCard, y: yBasedOnRow))
}
}
This code should create a grid of centers for your cards. You could build a function around it that returns a random center for a card to be placed and keeps track of used centers.
Potential improvements
First, I think that the centers array could be made a matrix ([[CGPoint]]()) for more logical storage of points.
Second, this code currently makes the assumption that the width and height of the container view are divisible by the card width and height. For example, a container width of 177 and a card width of 5 would result in some problems. The code could be fixed a number of different ways to account for this.
Best solution simplest/performance is to display card randomly BUT inside a grid. The trick is to have the grid bigger than the card size, so the card position inside the grid will be random.
Easy to check which position is occupy and cards will be on "random" frames.
1- Create a Collection View Controller with the total number of card u want to display (lets say.. max card that enter in the screen?)
2- Set the prototype cell size bigger than the card. If the card is 50x80 then the cell should be 70x110.
3- Add a UIImageView to the cell with constraints, this will be your card image
4- Create a UICollectionViewCell, with a method that set the card frames randomly inside the cell (modify the constraints)
Done!
Cells with no card will have no image or an empty cell as you wish. So to add a new card, just do a random between the empty cells and add the card with its random coordinates inside the cell.
Your UICollectionViewCell would like like this
class CardCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var card: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
let newX = CGFloat(arc4random_uniform(UInt32(bounds.size.width-card.bounds.size.width+1)))
let newY = CGFloat(arc4random_uniform(UInt32(bounds.size.height-card.bounds.size.height+1)))
card.leftAnchor.constraintEqualToAnchor(leftAnchor, constant: newX).active = true
card.topAnchor.constraintEqualToAnchor(rightAnchor, constant: newY).active = true
}
}
And your Collections View Controller should like like this
Collection View Image
As I can see in the picture all your cards are aligned at the bottom of the View. so if you generate a random y position from 0 to origin of your cards row - one card height you can simply get a CGPoint based on the frame of your view and size of the cards.
If you want to randomly place cards along the screen, you could do something like this:
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
let actualX = random(min: *whatever amount*, max: *whatever amount* )
let actualY = random(min: *Height of the first card*, max: *whatever amount* )
card.position = CGPoint(x: actualX, y: actualY )
The cards will then be positioned randomly above the existing cards.
I am not sure if you are planning to place all the cards in an orderly way. But if you do, you could do it like this.
Once the view is loaded, get all the possible card positions and store them in a map together with a number used as the key. Then you could generate a random number from 0 to the total number of possible card positions that you stored in the map. Then every time you occupy a position, clear a value from the map.
You can try with CAShapeLayer and UIBezierPath.
Create a CAShapeLayer for your main view where you will be adding sub views. Let's call it as main shape layer. This will be helpful to check the new view estimated is within the main view.
Create a UIBezierPath instance. Whenever a valid new sub view is found, add the edges to this path.
Create a random point within the main view.
Create a CGRect based on random point as center of your sub view. Let’s call it as estimated view frame.
Check the estimated view frame is completely visible in your main view. Else go to step 3.
Check your 4 edges of your estimated view frame with path object. If any one of the edge is inside the path, go to step 3.
If 4 edges are not inside the path, the estimated view frame is the new view’s frame.
Create a new subview and add it to your main view.
Add the edges of new view to path.
I have created a sample project in swift with the above logic.
https://github.com/mcabasheer/find-free-space-in-uiview
You can change the width and height of new view. I have added a condition to stop looking for next free space after trying 50 times. This will help to avoid infinite loop.
As #davecom highlighted, taking a random number to add new view will waste the space and you will run out of space quickly. If you are able to maintain the free available space, you can add more sub views.

How do I find out which direction a user is panning with UIPanGestureRecognizer?

So I am using UIPanGestureRecognizer in my project which I added to a view. I would like to know when a user either goes up, down,left or right. I am using the left and right feature to scrub through video. The up and down gesture is still to be determined. I have used the following code but I can't seem to figure it out. Thanks for the help!
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else {
// user dragged towards the left
print("left")
}
}
EDIT: Using Slider
if let duration = avPlayer?.currentItem?.duration {
let totalSeconds = CMTimeGetSeconds(duration)
let value = Float64(scrubberSlider.value) * totalSeconds
let seekTime = CMTime(value: Int64(value), timescale: 1)
avPlayer?.seek(to: seekTime, completionHandler: { (completedSeek) in
//perhaps do something later here
})
}
Joe's answer is close, but it won't take into account direct vertical or horizontal pans. (I'd comment on his answer except the formatting won't take.) Try this:
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else if vel.x < 0 {
// user dragged towards the left
print("left")
}
if vel.y > 0 {
// user dragged towards the down
print("down")
}
else vel.y < 0 {
// user dragged towards the up
print("up")
In essence, you are getting the CGPoint of the gesture (x,y) and determining the velocity of the movement. You have an alternative to this - taking the starting and ending point:
var startingPoint = CGPoint.zero
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
startingPoint = recognizer.location(in: self.videoView)
}
if recognizer.state == .ended {
let endingPoint = recognizer.location(in: self.videoView)
[ do the same comparing as above]
}
}
The advantage of the second option is you aren't doing unnecessary calculations during the pan. The disadvantage is that there are certain scenarios (like animating view movements) that are not conducive to it.
EDIT: I'm adding a bit more verbiage after reading your comment. It sounds to me that you may not be fully understanding what a pan gesture really is.
Like most (all?) gestures, it has a beginning, an in-between, and and end.
It is a two-dimensional drag with two components, both x and y.
There are actually SEVEN possible states, but FOUR of them (cancelled, failed, possible, recognized) do not happen with a pan gesture, leaving THREE states (began, changed, ended) that trigger.
I threw out one example - moving a view with a pan gesture - earlier. Now I'll try a second one - tracing an outline of, say, the Statue of Liberty in an image.
Here you want all THREE states, in order to know when to being tracing, when the path changes, and when it ends. And restricting this to the change state, I think you can see where both the X and the Y coordinate changes.
So yes, a logging of "left, up, left, up, left" is quite possible.I would think that if you traced a completely vertical line across the entire screen you might expect all "up" or "down" values in your log, but the odds of any human being panning that perfect is unlikely, so sure, a few "left" or "rights" may happen.
My tweak to Joe's code was to eliminate those moments of perfection. If vel.x == 0 you would have "left", and where bel.y == 0 you would have "down".
Again, if you simply want to know what the "result" of the pan is, use .began and .ended and ignore .changed - do not use recognizer.velocity but recognizer.state.
The "if" statements both of us gave you are really frameworks. If you understand both state and the two-dimensional nature of things, and you need to use .changed, then adapt those "if" statements - maybe compare the velocity of X to Y and take the greater, or eliminate those changes where the change in X or Y was under a threshold.
Try this code: tested in Swift 3.
Updated Answer: Below code will give you a starting and end location of your view when touch began.
if recognizer.state == .began {
let vel = recognizer.velocity(in: view) // view is your UIView
if vel.x > 0 {
print("right")
} else {
print("left")
}
}
if recognizer.state == .ended {
let vel = recognizer.velocity(in: view)
if vel.y > 0 {
print("down")
} else {
print("up")
}
}
Note : Your answer actually hidden in your code ?
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else {
// user dragged towards the left
print("left")
}
if vel.y > 0 {
// user dragged towards the down
print("down")
}
else {
// user dragged towards the up
print("up")
}
}
hope this helps...
Okay, now I'm getting the correct mental picture. You want scrub control. This is something very different, and I would recommend a UISlider over working with gestures - highly recommend it. For starters, they have the pan gesture already built in! Here's what I think apps like YouTube, QuickTime, etc. do.
(1) Let's take a specific example of having a video that is 1:53:22 in length, or (1*60*60)+(53*60)+22 = 6802 seconds in length.
(2) Add a "scrubber" subview to your main screen. You'll probably want a UISlider, two UILabels (one to each side of the slider), and anything else you think for a polished look.
(3) The UISLider will have a minimumValue of 0 seconds and a maximumValue of 6802 seconds. Of course, you'll want that max value to be calculated on each change of source.
(4) A question you'll want to answer for your app is whether to go the route of iTunes (where this scrubber view is always visible) or YouTube (where it is overly visible when the user or mouse cursor hovers over an area). For the former, you just need to position this scrub view in a position on the screen. For the latter though, you may wish to use a pan gesture - but only for visibility. Hold that thought.
(5a) You need two, maybe three more things on you UISlider. First is an automatic value update. Again it will depend on the visibility of the entire scrub view. You want to update, once a second, both the left hand UILabel and the UISLider value if it's always visible. For a disappearing one you probably can get away with only updating it once a second when it's visible.
(5b) The second thing you need to do with the UISlider is track changes the user makes to it (the "scrubbing") while it's visible. The event you are looking for is UIControl.valueChanged(). It will trigger anytime the user works with the slider, giving you the new seconds value to "scrub" the video to.
(5c) The third thing you might want to do with the UISlider is customize it a few ways - change the thumb image and the slider itself. My app changes the thumb image. These can only be done in code, there are no IB properties available.
Back to #4. All of the above doesn't need a pan gesture, unless you want the scrub view to appear only when needed.
If you have a mental picture of what I've described above, all you want to know is if a pan gesture has happened. No regards for direction. You might wish to have some regards for screen area - do want this scrub view to appear when a user pans over an area where the scrub view will not appear?
Wire up a CALayer (or the entire video view) with the pan gesture. Then code for a state of UIGestureRecognizer.began. Make the scrub view visible by changing it's alpha state from 0 to 1, or "sliding" it into view by changing it's origin or height. Add a UIView.animate(withDuration:) to it for a good effect.
Now, all that's left is setting the scrub view back to it's natural state. You'll need to code the reverse of whatever you did, and attach it to a timer set for however many seconds you want it visible.
TL;DR;
My app uses 4 UISliders that change various things (height, width, saturation, grill thickness) of of a photo effect that uses CoreImage. Performance is very tight, about 5/100 of a second to grab the new values of all 4 sliders and update the image.
These sliders are always visible today, but my next update (about 2 weeks away) will feature a "sliding control board" - think a keyboard with sliders and other controls on it. (There's limitations on the alpha value for a custom keyboard that forced me to write my own, but that's a separate discussion.)
So I know a "sliding scrub view" is possible. What I don't know for you is if you set the alpha value to a view to zero, will it detect pan gestures? I don't know, thus a CALayer may be needed.

How can I add sizing handles to a UIView?

I'm trying to dynamically create views (UIImageView and UITextView) at runtime by user request and then allow the user to move and resize them. I've got everything working great, except for the resizing. I tried using the pinch gesture recognizer, but find it too clumsy for what I want. Therefore, I would like to use sizing handles. I believe I could put a pan gesture recognizer on each handle, and adjust the view frame as one of them is moved.
The problem is, I'm not quite sure how to create the sizing handles. I would indicate all the things I've tried, but truthfully, I'm not quite sure where to start. I do have a few ideas...
1) Possibly use coregraphics to draw boxes or circles on the corners and sides? Would I create a new layer and draw them on that? Not sure.
2) Stick a little image of a box or circle on each corner?
3) XIB file with the handles already placed on it?
Any suggestions appreciated. I just need to be pointed in the right direction.
Edit: Something like what Apple uses in Pages would be perfect!
First, I suggest create a custom View subclass to UIView, you will handle all of the behavior here. Let's call it ResizableView.
In the custom view, You need to draw layer or view for these dot at corner and add PangestureRecognized to them.Then you can track the location of these dot using recognizer.locationInView() when user drag them, which you will use to calculate the scale of View.Here is the code you can refer to:
func rotateViewPanGesture(recognizer: UIPanGestureRecognizer) {
touchLocation = recognizer.locationInView(self.superview)
let center = CalculateFunctions.CGRectGetCenter(self.frame)
switch recognizer.state {
case .Began:
initialBounds = self.bounds
initialDistance = CalculateFunctions.CGpointGetDistance(center, point2: touchLocation!)
case .Changed:
//Finding scale between current touchPoint and previous touchPoint
let scale = sqrtf(Float(CalculateFunctions.CGpointGetDistance(center, point2: touchLocation!)) / Float(initialDistance!))
let scaleRect = CalculateFunctions.CGRectScale(initialBounds!, wScale: CGFloat(scale), hScale: CGFloat(scale))
self.bounds = scaleRect
self.refresh()
case:.Ended:
self.refresh()
default:break
Step by step
touchLocation location of the Pangesture
center is the center of ResizableView
initialBounds is the initial bounds of the ResizableView when PanGesture begin.
initailDistance is the distance between the center of the ResizableView of touchPoint of the dot the user is dragging.
Then you can calculate the scale given initialDistance, center, touch location
Now you have scaled the view as You want. You also need to refresh the position of these dot at corner accordingly, that's what refresh() for, you need to implement it yourself.
CalculateFunctions
I tend to define some helpFunctions to help me calculate.
CalculateFunctions.CGPointGetDistance is used to calculate the distance between center of the view and touch location.
CalculateFunctions.CGRectScale return the scaled CGRect given the the scale you just calculated.
CalculateFunctions.CGRectGetCenter return the center point of a CGRect
That's just a rough idea. Actually there are many Libraries you can refer to.
Some suggestions:
SPUserResizableView
This is a ResizableView exactly what you want, but it was written in ObjC and hasn't been updated for a long time. But you can refer to it.
JLStickerTextView This may not fit your requirement very well as it is for text(edit, resize, rotate with one finger) However, this one is written in Swift, a good example for you.
If you have any questions, feel free to post it.Good Luck:-)

Resize a UIView from only the ends using a UIPanGestureRecognizer

I have a vertical line (UIView) which I need to resize. I currently do it using a pinch gesture but now I need to do it using a swipe gesture and increase or decrease the height from the side I swipe from. So for example, if I swipe down from the TOP END of the line, the line must decrease in size only from the top. The bottom must stay anchored to the original position.
How do I do this?
I also need to be able to move the image around, which I already have implemented using a UIPanGestureRecognizer as well.
Here's what I've playing around with:
I have a small UIView on top end of the vertical line and one on the bottom end of the line. I use these as markers. Now, I'm able to move the top marker up and down. I need to resize the line to the distance between the two markers AND keep the bottom end of the line at the same position as the bottom marker.
This is what I have
func draggedViewForTopMarker(sender: UIPanGestureRecognizer) {
//To move the top marker
var translation = sender.translationInView(self.view)
sender.view!.center = CGPointMake(sender.view!.center.x, sender.view!.center.y + translation.y)
sender.setTranslation(CGPointZero, inView: self.view)
//Find distance between the markers
var distanceBetweenMarkers = (sender.view!.center.y + translation.y) - self.bottomMarker.center.y
print(distanceBetweenMarkers)
if (distanceBetweenMarkers < 0) {
distanceBetweenMarkers = distanceBetweenMarkers*(-1)
}
//**TRYING** to resize the line to have the same height as the distance between the two markers AND make sure its positioned between the markers and the bottom end of the line is still in the same place as it was originally.
var newFrame = CGRectMake(sender.view!.center.x, distanceBetweenMarkers/2, vertical.frame.width, distanceBetweenMarkers)
vertical.frame = newFrame
}
I'm open to different approaches too, or a solution to the problem I have with this approach!
You may check out at Photo Measures Lite on the App Store to better understand what I mean when I say "resize the line from one side only".
Thanks so much!
instead of making a new frame you can just adjust y position of the line to be the same as the top marker, and the height to be the same as the distance between the two markers
func draggedViewForTopMarker(sender: UIPanGestureRecognizer) {
//To move the top marker
var translation = sender.translationInView(self.view)
sender.view!.center = CGPointMake(sender.view!.center.x, sender.view!.center.y + translation.y)
sender.setTranslation(CGPointZero, inView: self.view)
//Find distance between the markers
var distanceBetweenMarkers = self.topMarker.frame.origin.y - self.bottomMarker.frame.origin.y
vertical.frame.origin.y = self.topMarker.frame.origin.y
vertical.frame.size.height = distanceBetweenMarkers
}
But this will only work if the two markers and the vertical line are the subviews of the same superview

Resources