[Swift-iOS]Dragging down view using UIPanGestureRecognizer, not stretching - ios

I'm trying to drag down a specific view to make the height go longer than before using UIPanGestureRecognizer. However, it keeps on going back to the normal size that i had initially programmed. The view that i'm trying to stretch keeps on shaking and goes back to the initial height. Any guesses why?
let CalendarDrag = UIPanGestureRecognizer(target: self, action: #selector(didDragCalendar))
lineView.isUserInteractionEnabled = true
lineView.addGestureRecognizer(CalendarDrag)
}
#objc func didDragCalendar(sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: self.view) //속도
let translation = sender.translation(in: self.view) //μœ„μΉ˜
let height = self.topView.frame.maxY
if sender.state == .ended{
if velocity.y>0{
calendar.scope = .month
print("down")
}else{
calendar.scope = .week
print("up")
}
}else{
if height <= height+translation.y && height+translation.y <= height+230{
self.topView.frame = CGRect(x: 0, y: 0, width: self.topView.frame.width, height: height+translation.y)
UIView.animate(withDuration: 0, animations: {
self.view.layoutIfNeeded()
sender.setTranslation(CGPoint.zero, in: self.view)
})
}
}
}
For more information, I'm right now trying to make FSCalendar to show in "month" and "week" view according to the UIPanGesture that user make.

You are changing the frame height of your view. Don't do that.
Have an instance var to a height constraint, and change the constant value for that constraint. Then when you call layoutIfNeeded() from your animation, the constraint will change your height for you.

Related

Want to restrict area of pan gesture on one imageView swift 5

I have one imageview which has 2 gestureRecognizers
1)Pinch
2)Pan
I am able to pinch an image and zoom using scale property
what I am not able to achieve is that when I zoom that image and drag to all 4 sides I can drag image and I am able to see the background view behind the imageview
want to restrict drag of the imageview till that zommed image is shown
Here is the code for pinch and pan gesture
#objc func pinchRecognized(pinch: UIPinchGestureRecognizer) {
if let view = pinch.view {
view.transform = view.transform.scaledBy(x: pinch.scale, y: pinch.scale)
pinch.scale = 1
}
}
#objc func PangestureMethod(gestureRecognizer: UIPanGestureRecognizer){
guard gestureRecognizer.view != nil else {return}
let piece = gestureRecognizer.view!
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
self.initialCenter = piece.center
}
if gestureRecognizer.state != .cancelled {
let newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
piece.center = newCenter
}
else {
piece.center = initialCenter
}
}
First, it's (probably) easier to reset the Pan Gesture's Translation so we're calculating relative movement:
if gestureRecognizer.state != .cancelled {
// translation will be + or - a small number of points
// do what's needed to move the view
// reset recognizer
gestureRecognizer.setTranslation(.zero, in: superV)
}
Otherwise, let's say you drag (pan) right 300-pts, but the view can only move 20-pts, then it won't start moving back to the left until you've dragged 280-pts left.
So, when dragging horizontally and we want to stop when the drag-view is at the left or right edge of its superView...
calculate MAX centerX position
that will be 1/2 of the drag-view's width
calculate MIN centerX position
that will be width-of-superView minus 1/2 of the drag-view's width
As an example, if the superView's width is 100, and the drag-view's width is 200, the MAX centerX will be 100 and the MIN centerX will be Zero.
Then we do the same thing for the centerY position.
Try using this as your Pan Gesture handler:
#objc func PangestureMethod(gestureRecognizer: UIPanGestureRecognizer){
// unwrap the view from the gesture
// AND
// unwrap that view's superView
guard let piece = gestureRecognizer.view,
let superV = piece.superview
else {
return
}
let translation = gestureRecognizer.translation(in: superV)
if gestureRecognizer.state == .began {
self.initialCenter = piece.center
}
if gestureRecognizer.state != .cancelled {
// what the new centerX and centerY will be
var newX: CGFloat = piece.center.x + translation.x
var newY: CGFloat = piece.center.y + translation.y
// MAX centerX is 1/2 the width of the piece's frame
let mxX = piece.frame.width * 0.5
// MIN centerX is Width of superView minus 1/2 the width of the piece's frame
let mnX = superV.bounds.width - piece.frame.width * 0.5
// make sure new centerX is neither greater than MAX nor less than MIN
newX = max(min(newX, mxX), mnX)
// MAX centerY is 1/2 the height of the piece's frame
let mxY = piece.frame.height * 0.5
// MIN centerY is Height of superView minus 1/2 the height of the piece's frame
let mnY = superV.bounds.height - piece.frame.height * 0.5
// make sure new centerY is neither greater than MAX nor less than MIN
newY = max(min(newY, mxY), mnY)
// set the new center
piece.center = CGPoint(x: newX, y: newY)
// reset recognizer
gestureRecognizer.setTranslation(.zero, in: superV)
}
else {
piece.center = initialCenter
}
}
Edit
To prevent the Pinch Gesture from scaling down the view to smaller than its superView frame, we can apply the new scale value from the gesture to a CGRect of the view's frame before applying it to the view.
Then, only apply the scaling to the view if the resulting rect would not be smaller than the superView's frame.
Give this a try:
#objc func pinchRecognized(pinch: UIPinchGestureRecognizer) {
// unwrap the view from the gesture
// AND
// unwrap that view's superView
guard let piece = pinch.view,
let superV = piece.superview
else {
return
}
// this is a bit verbose for clarity
// get the new scale
let sc = pinch.scale
// get current frame of "piece" view
let currentPieceRect = piece.frame
// apply scaling transform to the rect
let futureRect = currentPieceRect.applying(CGAffineTransform(scaleX: sc, y: sc))
// if the resulting rect's width will be
// greater-than-or-equal to superView's width
if futureRect.width >= superV.bounds.width {
// go ahead and scale the piece view
piece.transform = piece.transform.scaledBy(x: sc, y: sc)
}
pinch.scale = 1
}

iOS -How to have a separate UIView take control of a UISlider's thumbRect

I have a button that I press and a custom alert appears with a normal UISlider inside of it. There are some other views and labels that I have that show the distance etc that are above and behind the slider. What happens is the thumbRect of the slider doesn't slide to well when touched. I have to be very accurate when trying to slide it and it seems buggy. What I want to do is add a clear UIView in front of it (and the other views and labels above it) and have the clear UIView take control of the slider using a UIGestureRecognizer.
Here's the setup so far. The clearView is what I want to use to take control of the slider. Right now it's behind everything and I colored the clearView red just so it's visible for the example
bgView.addSubview(slider) // slider added to bgView
let trackRect = slider.trackRect(forBounds: slider.frame)
let thumbRect = slider.thumbRect(forBounds: slider.bounds, trackRect: trackRect, value: slider.value)
milesLabel.center = CGPoint(x: thumbRect.midX, y: slider.frame.origin.y - 55)
bg.addSubview(milesLabel) // milesLabel added to bgView
// ** all the other views you see are added to the bgView and aligned with the thumbRect's center **
let milesLabelRect = bgView.convert(milesLabel.frame, to: self.view)
let sliderRect = bgView.convert(slider.frame, to: self.view)
let topOfMilesLabel = milesLabelRect.origin.y
let bottomOfSlider = sliderRect.maxY
let distance = bottomOfSlider - topOfMilesLabel
clearView.frame = CGRect(x: milesLabel.frame.minX, y: milesLabel.frame.minY, width: milesLabel.frame.width, height: distance)
bgView.addSubview(clearView) // clearView added to bgView
bgView.insertSubview(clearView, at: 0)
When I slide the slider everything successfully slides with it including the clearView but of course the thumbRect is still in control of everything.
#objc func onSliderValChanged(sender: UISlider, event: UIEvent) {
slider.value = round(sender.value)
UIView.animate(withDuration: 0.25, animations: {
self.slider.layoutIfNeeded()
let trackRect = sender.trackRect(forBounds: sender.frame)
let thumbRect = sender.thumbRect(forBounds: sender.bounds, trackRect: trackRect, value: sender.value)
self.milesLabel.center = CGPoint(x: thumbRect.midX, y: sender.frame.origin.y - 55)
// ** all the other views are aligned with the thumbRect's center as it slides **
self.clearView.frame.origin = self.milesLabel.frame.origin
})
}
The idea is to move the clearView to the front of everything and use that to control the thumbRect (don't forget it's only red for the example).
bgView.bringSubviewToFront(clearView)
// clearView.backgroundColor = UIColor.clear
I tried to use a UIPanGesture and a UILongPressGestureRecognizer to have the clearView take control of the slider but when I slide the slider only the thumbRect slides, the clearView stays still. Look where the clearView (which is red) is and look where the thumbRect is after I slid it to the 10 mile point.
// separately tried a UILongPressGestureRecognizer too
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
clearView.isUserInteractionEnabled = true
clearView.addGestureRecognizer(panGesture)
#objc func draggedView(_ sender: UIPanGestureRecognizer) {
if slider.isHighlighted { return }
let point = sender.location(in: slider)
let percentage = Float(point.x / slider.frame.width)
let delta = percentage * (slider.maximumValue - slider.minimumValue)
let value = slider.minimumValue + delta
slider.setValue(value, animated: true)
}
If I play around with it enough the clearView sometimes slides with the the thumbRect but they definitely aren't in alignment and it's very buggy. I also lost control of all the other views the were aligned with the thumbRect.
How can I pass control from the slider's thumbRect to the clearView?
You might have your mind set on doing it this way, which is okay. But I have a fast alternative for you that might suit your needs.
If you subclass your playback slider and override the point() method you can increase the surface area of the slider's thumb.
class CustomPlaybackSlider: UISlider {
// Will increase target surface area for thumb.
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds: CGRect = self.bounds
// Set the inset to a negative value by how much you want the surface area to increase by.
bounds = bounds.insetBy(dx: -10, dy: -15)
return bounds.contains(point)
}
}
Might simplify things for you. Although, the only downside of this approach is that the surface area for the touch would extend equally down/up for your dx value and equally left/right for your dy value.
Hope this helps.
I found the key to the problem here in this answer. #PaulaHasstenteufel said
// also remember to call valueChanged if there's any
// custom behaviour going on there and pass the slider
// variable as the parameter, as indicated below
self.sliderValueChanged(slider)
What I had to do was
Use a UIPanGestureRecognizer
comment out the UISlider's target action
use the code from the target action's selector method in step-4 instead of using it in #objc func onSliderValChanged(sender: UISlider, event: UIEvent { }
take the code from step-3 and add it to a new function
add the function from step-5 to the bottom of the UIPanGetstureRecognizer's method
1- use the UIPanGestureRecognizer and make userInteraction is enabled for the UIView
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
clearView.isUserInteractionEnabled = true
clearView.addGestureRecognizer(panGesture)
2- the Slider, I commented out it's addTarget for the .valueChanged event
let slider: UISlider = {
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumValue = 1
slider.maximumValue = 5
slider.value = 1
slider.isContinuous = true
slider.maximumTrackTintColor = UIColor(white: 0, alpha: 0.3)
// ** COMMENT THIS OUT **
//slider.addTarget(self, action: #selector(onSliderValChanged(sender:event:)), for: .valueChanged)
return slider
}()
3- Take the code from the selector method the targetAction from above was using and instead insert that code in step 4
#objc func onSliderValChanged(sender: UISlider, event: UIEvent) {
/*
use this code in step-4 instead
slider.value = round(sender.value)
UIView.animate(withDuration: 0.25, animations: {
self.slider.layoutIfNeeded()
let trackRect = sender.trackRect(forBounds: sender.frame)
let thumbRect = sender.thumbRect(forBounds: sender.bounds, trackRect: trackRect, value: sender.value)
self.milesLabel.center = CGPoint(x: thumbRect.midX, y: sender.frame.origin.y - 55)
// ** all the other views are aligned with the thumbRect's center as it slides **
self.clearView.frame.origin = self.milesLabel.frame.origin
})
*/
}
4- Create a new method and insert the code from step-3
func sliderValueChanged(_ sender: UISlider, value: Float) {
// round the value for smooth sliding
sender.value = round(value)
UIView.animate(withDuration: 0.25, animations: {
self.slider.layoutIfNeeded()
let trackRect = sender.trackRect(forBounds: sender.frame)
let thumbRect = sender.thumbRect(forBounds: sender.bounds, trackRect: trackRect, value: sender.value)
self.milesLabel.center = CGPoint(x: thumbRect.midX, y: sender.frame.origin.y - 55)
// ** all the other views are aligned with the thumbRect's center as it slides **
self.clearView.frame.origin = self.milesLabel.frame.origin
})
}
5- I used the same code that I used for the UIGestureRecognizer but commented out the setValue function and instead used the function from step 4
#objc fileprivate func draggedView(_ sender: UIPanGestureRecognizer) {
if slider.isHighlighted { return }
let point = sender.location(in: slider)
let percentage = Float(point.x / slider.bounds.size.width)
let delta = percentage * (slider.maximumValue - slider.minimumValue)
let value = slider.minimumValue + delta
// I had to comment this out because the siding was to abrupt
//slider.setValue(value, animated: true)
// ** CALLING THIS FUNCTION FROM STEP-3 IS THE KEY TO GET THE UIView TO CONTROL THE SLIDER **
sliderValueChanged(slider, value: value)
}

Resize MapView on tap

I have mapView to my ViewController in storyboard. It has 4 constraints ( Height equals: 300 , align trailing to: Safe Area, align Leading to: Safe Area and Align Top to: Safe Area. I created tapGestureRecognizer to my mapview. I want to resize mapview to full screen when user tap on a map.
private func setupMapView() {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(mapTouchAction(gestureRecognizer:)))
mapView.addGestureRecognizer(gestureRecognizer)
}
#objc func mapTouchAction(gestureRecognizer: UITapGestureRecognizer) {
}
I tried to change height constraint to screen Height but it is not what i want ( view is positioning in the middle of the screen and then its resizing , this is not what i am looking for) I used this code inside my mapTouchAction(). I want my view to resize with animation to the bottom view ( screen height). How can i achieve it? should i use CGAffineTransform or maybe theres another way? Any ideas?
for constraint in self.mapView.constraints {
if constraint.identifier == "mapViewHeightConstraintID" {
constraint.constant = UIScreen.main.bounds.height
}
}
UIView.animate(withDuration: 2, animations: {
self.mapView.layoutIfNeeded()
})
I found an easy solution for my problem. I only change my mapView frame. So inside tapGesture function mapTouchAction(_: UITapGestureRecognizer) i have
#objc func mapTouchAction(gestureRecognizer: UITapGestureRecognizer) {
UIView.animate(withDuration: 2, animations: {
self.mapView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
self.mapView.layoutIfNeeded()
})
}
This code change my mapView height to UIScreen Height without with a nice resizing animation for 2 seconds.

Stop UIDynamicAnimator Whit view Limits

I have been trying to create a custom UIClass that implements an horizontal scrolling for its inner content by using UIPanGestureRecognizer.
My first success approach was:
#IBAction func dragAction(_ sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: sender.view)
if velocity.x > 0{
//print("\(logClassName) Dragging Right ...")
if innerView!.frame.origin.x < CGFloat(0){
offSetTotal += 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
else{
//print("\(logClassName) Dragging Left ...")
if totalWidth - innerView!.bounds.maxX < innerView!.frame.origin.x{
offSetTotal -= 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
}
That way, althought is a little bit clunky, I am able to scroll from left to right (and reverse) and the innerview is always covering in the holderView.
Then, and because i wanted to get the scrolling effect that you would get with a UIScrolling view i tried the following approach
#objc func handleTap(_ pan: UIPanGestureRecognizer){
let translation = pan.translation(in: innerView)
let recogView = pan.view
let curX = recogView?.frame.origin.x
if pan.state == UIGestureRecognizerState.began {
self.animator.removeAllBehaviors()
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.changed {
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.ended {
let itemBehavior = UIDynamicItemBehavior(items: [innerView!]);
var velocity = pan.velocity(in: self)
print(velocity.y)
velocity.y = 0
itemBehavior.addLinearVelocity(velocity, for: innerView!);
itemBehavior.friction = 1
itemBehavior.resistance = 1;
//itemBehavior.elasticity = 0.8;
self.collision = UICollisionBehavior(items: [innerView!]);
self.collision!.collisionMode = .boundaries
self.animator.addBehavior(collision!)
self.animator.addBehavior(itemBehavior);
}
Now the problem i face is that it moves horizontally but it goes away and gets lose.
Is that the right approach? Is there a delegate?
The question was posted due to a lack of knowledge of UIScrollViews. I Knew how to do with Storyboard and only scroll vertically.
I ended up creating a programatically UIScrollView and setting up its content size with the inner view.
scrollView = UIScrollView(frame: bounds)
scrollView?.backgroundColor = UIColor.yellow
scrollView?.contentSize = innerView!.bounds.size
scrollView?.addSubview(innerView!)
addSubview(scrollView!)
But first I have defined the innerView.

Swift: How to zoom an image as I scroll up in scrollview

I'm trying to zoom my image view as I scroll the scrollView past the top of the screen. Here's my code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
if (offset <= 0) {
let ratio: CGFloat = -offset*1.0 / UIScreen.main.bounds.height
self.coverImageView.transform = CGAffineTransform(scaleX: 1.0 + ratio, y: 1.0 + ratio)
}
}
This zooms the image as I scroll up, but because I am also scrolling up, my view goes down, and reveals the white background behind the image as it expands. How do I prevent that from happening?
It sounds like a scroll view is not really suited to what you are trying to do. How about using a gesture recognizer instead? Something along these lines:
override func viewDidLoad() {
super.viewDidLoad()
coverImageView.isUserInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan))
coverImageView.addGestureRecognizer(panGestureRecognizer)
}
func didPan(panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: coverImageView)
if translation.y > 0 {
let zoomRatio = (translation.y * 0.1) + 1.0
coverImageView.transform = CGAffineTransform(scaleX: zoomRatio, y: zoomRatio)
}
}
You'll have to play around to get it to behave exactly how you want, but it should be enough to get you started.

Resources