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.
Related
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
I have a subview in a viewController to which I added a UIPanGestureRecognizer. I'd like to expand such subview when the user drags it down, so I'm trying to handle the gesture recognizer status like this:
#objc func panning(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: view)
if gestureRecognizer.state == .began {
originalFrame = self.mySubview.frame
} else if gestureRecognizer.state == .changed {
if translation.y > 0 {
var newFrame = originalFrame
newFrame.size.height += translation.y
mySubview.frame = newFrame
}
}
}
when I set a breakpoint at mySubview.frame = newFrame, I see that newFrame is updated with the translation. However, I don't see the frame changes in the screen, the subview looks always like at the beginning.
What could I be missing?
PanGesture is a tricky, but working with it is very interesting...
I am assuming you have added pan gesture correctly to the view. So writing down the handling of the gesture only.
func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
var translatedPoint: CGPoint = gesture.translation(in: self.superview)
let playerCenterX: CGFloat = (gesture.view?.center.x)! + translatedPoint.x
let playerCenterY: CGFloat = (gesture.view?.center.y)! + translatedPoint.y
translatedPoint = CGPoint.init(x: playerCenterX,
y: playerCenterY)
gesture.view?.center = translatedPoint
gesture.setTranslation(CGPoint.zero, in: self.superview)
}
Important thing is that you set its translation back to zero every time at the end of gesture movement.
As per your requirement change this self.superview and use the coordinates to update your view frame...
If your view's frame is set by the autolayout you should update constraints, not the view's frame itself.
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.
I am attempting, when the keyboard appears, to shift the view up. This works on two views, but on the third the same code causing the view to seemingly move down a certain amount, then move back into the exact place it started, or so the animation seems. Debugging, I see nowhere else the self.view.frame is getting set but this method. In addition, the offsets look right, as if the view should move up like the other views have. See the keyboardWillShow method below.
func keyboardWillShow(notification: NSNotification){
if self.origFrame == nil{
self.origFrame = self.view.frame
}
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue(){
var testRect = self.view.frame
testRect.size.height -= keyboardSize.height
if !testRect.contains(loginBtn!.frame.origin){
let bottomSpace = self.view.frame.size.height - loginBtn.frame.origin.y - loginBtn.frame.size.height
let keyboardOverlap = keyboardSize.height - bottomSpace
let newY = self.origFrame!.origin.y - keyboardOverlap
self.view.frame.origin.y = newY
}
}
}
This is what I've done in the past
func textViewDidBeginEditing(textView: UITextView) {
let point:CGPoint = CGPoint(x: textView.frame.origin.x - 8, y: textView.frame.origin.y - 100)
scrollView.contentOffset = point
}
This moves the view up based on the position of textview the user tapped on. 8 and 100 pixels just happened to be good ranges for my specific purpose.
Alternatively, instead of moving the frames, you could programmatically adjust your constraints.
First of all I have checked almost every places over the internet but I didn't get any solution about this topic.
In my cases I have multiple UIView objects inside a superview or you can say a canvas where I am drawing this views.
All this views are attached with pan gesture so they can be moved inside anywhere of their superview.
Some of this views can be rotated using either rotation gesture or CGAffineTransformRotate.
Whenever any of the view will be outside of the main view then it will be deleted.
Now following are my code.
#IBOutlet weak var mainView: UIView!
var newViewToAdd = UIView()
newViewToAdd.layer.masksToBounds = true
var transForm = CGAffineTransformIdentity
transForm = CGAffineTransformScale(transForm, 0.8, 1)
transForm = CGAffineTransformRotate(transForm, CGFloat(M_PI_4)) //Making the transformation
newViewToAdd.layer.shouldRasterize = true //Making the view edges smooth after applying transforamtion
newViewToAdd.transform = transForm
self.mainView.addSubview(newViewToAdd) //Adding the view to the main view.
Now in case the gesture recognizer its inside the custom UIView Class -
var lastLocation: CGPoint = CGPointMake(0, 0)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.superview?.bringSubviewToFront(self)
lastLocation = self.center //Getting the last center point of the view on first touch.
}
func detectPan(recognizer: UIPanGestureRecognizer){
let translation = recognizer.translationInView(self.superview!) //Making the translation
self.center = CGPointMake(lastLocation.x + translation.x, lastLocation.y + translation.y) //Updating the center point.
switch(recognizer.state){
case .Began:
break
case .Changed:
//MARK: - Checking The view is outside of the Superview or not
if (!CGRectEqualToRect(CGRectIntersection(self.superview!.bounds, self.frame), self.frame)) //if its true then view background color will be changed else background will be replaced.
{
self.backgroundColor = outsideTheViewColor
var imageViewBin : UIImageView
imageViewBin = UIImageView(frame:CGRectMake(0, 0, 20, 25));
imageViewBin.image = UIImage(named:"GarbageBin")
imageViewBin.center = CGPointMake(self.frame.width/2, self.frame.height/2)
addSubview(imageViewBin)
}else{
for subViews in self.subviews{
if subViews.isKindOfClass(UIImageView){
subViews.removeFromSuperview()
}
self.backgroundColor = deSelectedColorForTable
}
}
case .Ended:
if (!CGRectEqualToRect(CGRectIntersection(self.superview!.bounds, self.frame), self.frame)) //If its true then the view will be deleted.
{
self.removeFromSuperview()
}
default: break
}
}
The main problem is if the view is not rotated or transformed then all the "CGRectIntersection" inside the .Changed/.Ended case is working fine as expected but if the view is rotated or transformed then "CGRectIntersection" always becoming true even the view is inside the "mainView" and its removing from the mainview/superview.
Please help about my mistake.
Thanks in advance.
Frame of the view gets updated after applying transform. Following code ensures that it is inside the its superviews bounds.
if (CGRectContainsRect(self.superview!.bounds, self.frame))
{
//view is inside of the Superview
}
else
{
//view is outside of the Superview
}