I have a hidden UIView at the bottom of my UIViewController.
I would like to create an interactive animation that would show progressively my UIView, like if I was dragging the UIView from the bottom and it would follow my finger (location, speed, etc.). If I go over half way the final location of my UIView and release my finger, it would continue, if I don't reach this half way, it would go back hidden. Basically, the same behaviour like the control center.
The problem is I don't know where to start. Can someone point me to the right direction?
You should use UIGestureRecognizer,for example UIScreenEdgePanGestureRecognizer
keep a property
var gestureReconginzer:UIScreenEdgePanGestureRecognizer?
Then in viewDidLoad,init the gestureReconginzer
gestureReconginzer = UIScreenEdgePanGestureRecognizer(target: self, action: "catch:")
gestureReconginzer?.edges = UIRectEdge.Bottom
self.view.addGestureRecognizer(gestureReconginzer!)
When the gesture is Reconginzed
func catchGestrue(gesture:UIScreenEdgePanGestureRecognizer){
switch(gesture.state){
case .Began:
//Set your view hidden = false
case .Changed:
//Change your view center
case .Ended:
//Decide if your view reach half way.
//Use UIView.animateWithDuration to let your view return or go to right place
default:
}
}
BTY: I do not think pull a view from bottom is a good idea.
I have a simple objective C project in here,if you know objective C,you may refer the gesture part.
Related
In UIScrollView, there is a feature named "Keyboard Dismiss interactively"
By using such option, this enables me to implement the following drag down to hide keyboard
However, the keyboard dismiss operation only kick start, when the UIScrollView drag action touches keyboard edge.
What I would like to achieve is, the keyboard dismiss operation kick start, when the UIScrollView drag operation touches the bottom toolbar edge.
What I wish to achieve (Same as WhatsApp)
As you can see from the video, the keyboard dismiss operation will kick start, when the drag operation touches the bottom bar edge, even before touching keyboard edge.
May I know, what technique WhatsApp is using, to achieve such behavior?
Side note
You may notice our bottom toolbar does move along with keyboard. This is because there is a bottom constraint for bottom toolbar's bottom with Safe Area's bottom.
We adjust the bottom constraint's constant value, by installing a gesture recognizer in global Window. This is the code snippet to achieve such technique.
#objc private func didPan(_ sender: UIPanGestureRecognizer){
if keyboardHeight > 0 {
let mainScrollView = editable.mainScrollView
let isScrolling = (mainScrollView.isDragging || mainScrollView.isDecelerating)
if isScrolling {
if let mainScrollViewGlobalOrigin = mainScrollView.globalOrigin {
let point = sender.location(in: sender.view!)
// Take safe area into consideration, like iPhone 12 Pro Max.
let key = UIWindow.key
let bottomSafeArea = key?.safeAreaInsets.bottom ?? 0
let dy = point.y - (
mainScrollViewGlobalOrigin.y +
mainScrollView.frame.height +
toolbarHeightLayoutConstraint.constant +
bottomSafeArea -
bottomLayoutConstraint.constant -
self.keyboardHeight
)
if dy > 0 {
bottomLayoutConstraint.constant = -(keyboardHeight - dy)
}
}
}
}
}
The reason that WhatsApp behaves like this is that their view is considered to be part of the keyboard, so when the swipe gesture reaches their custom view it will begin interactive dismissal.
To achieve this yourself all you need to do is provide the toolbar view as the inputAccessoryView for your view controller. You won't need the constraints for positioning as the keyboard window would then control your toolbar's position.
There is also inputAccessoryViewController for the times where your toolbar may not be a UIView, but instead an entire UIViewController.
The views in either of these properties will only be visible when the keyboard is visible, so to get around that you'll still want to put it into your view hierarchy, but remove/add it based on becoming/resigning first responder.
EDIT: Also, you should be using UIApplication.keyboardDidChangeFrameNotification to detect when the keyboard changes size/position/etc and allow you to adjust insets/positions of views appropriately. In modern iOS there are plenty of ways the keyboard can change size while open, and observing that notification is the correct way to handle the keyboard size.
The Problem
I have a UIScrollView containing a UIView that I wish to allow the user to pan using a UIPanGestureRecognizer.
In order for this to work as desired, users should be able to pan the view with one finger, but also be able to pan the scroll view with another finger - doing both at the same time (using one finger for each).
However, the scroll view ceases to work when the user is panning a view contained within it. It cannot be panned until the view's pan gesture ends.
Attempted Workaround
I tried to work around this by enabling simultaneous scrolling of both the pan view and the UIScrollView that contains it by overriding the following UIGestureRecognizerDelegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
However, this makes it so that panning the view also moves the scroll view. Each element's panning gesture should be independent of the other, not linked.
Demo Project
I have created a simple demo project that should demonstrate this, here:
https://github.com/jeffc-dev/ScrollViewPannerTest
This project contains a scroll view with a square view that should be able to be panned independently of its containing scroll view, but can not.
Why I'm Doing This
The point of this is to make it easier/quicker for a user to find a destination to pan the view to. The is somewhat analogous to rearranging icons in Springboard: You can use one finger to pan an app icon while simultaneously panning between pages with another finger, quickly finding a place to drop it. I'm not using a paged scroll view - just a normal one - and I want it to be a seamless panning gesture (I don't need/want the user to have to enter a 'wiggle mode') but the basic principle is the same.
UPDATE: DonMag helpfully came up with the idea of using a UILongPressGestureRecognizer to move the view out of the scroll view for panning, which does seem promising. However, if I went that route I think I'd need to seamlessly transition to using a UIPanGestureRecognizer after doing so (as I do use some pan gesture recognizer-specific functionality).
I'm sure there are different ways to do this, but here is one approach...
Instead of using a UIPanGesture I used a UILongPressGesture.
When the gesture begins, we move the view from the scrollView to its superview. While we continue to press the view and drag it around, it is now independent of the scrollView. When we end the gesture (lift the finger), we add the view back to the scrollView.
While dragging, we can use a second finger to scroll the content of the scroll view.
The main portion of the code looks like this:
#objc func handleLongPress(_ g: UILongPressGestureRecognizer) -> Void {
switch g.state {
case .began:
// get our superview and its superview
guard let sv = superview as? UIScrollView,
let ssv = sv.superview
else {
return
}
theScrollView = sv
theRootView = ssv
// convert center coords
let cvtCenter = theScrollView.convert(self.center, to: theRootView)
self.center = cvtCenter
curCenter = self.center
// add self to ssv (removes self from sv)
ssv.addSubview(self)
// start wiggling anim
startAnim()
// inform the controller
startCallback?(self)
case .changed:
guard let thisView = g.view else {
return
}
// get the gesture point
let point = g.location(in: thisView.superview)
// Calculate new center position
var newCenter = thisView.center;
newCenter.x += point.x - curCenter.x;
newCenter.y += point.y - curCenter.y;
// Update view center
thisView.center = newCenter
curCenter = newCenter
// inform the controller
movedCallback?(self)
default:
// stop wiggle anim
stopAnim()
// convert center to scroll view (original superview) coords
let cvtCenter = theRootView.convert(curCenter, to: theScrollView)
// update center
self.center = cvtCenter
// add self back to scroll view
theScrollView.addSubview(self)
// inform the controller
endedCallback?(self)
}
}
I forked your GitHub repo and added a new controller to demonstrate: https://github.com/DonMag/ScrollViewPannerTest
You'll see that it is just a Starting Point for this approach. The view being dragged (actually, in this demo, you can use two fingers to drag two views at the same time) uses closures to inform the controller about the dragging...
Currently, "drag/drop" does not affect any other subviews in the scrollView. The only closure that does anything is the "ended" closure, at which point the controller re-calcs the scrollView's contentSize. The "moved" closure could be used to re-position views -- but that's another task.
Is there a way in iOS to run a fade animation on a UIView (UILabel specifically) that will fade in with a swipe action (so if it paused half way, the left side is visible, right side invisible and middle some gradient between).
I'm wondering if theres a Gradient mask I could use with the Alpha channel?
Any ideas or code snippets for achieving this sort of reveal? Vertical or Horizontal.
(Link to the same question but for Android for reference - Android - How to fade in a View with a horizontal swipe/fade?)
Simple hacky solution will be with additional view which can be placed on top of the label. The way that you can do it is to create this view and to animate it is movement by X axis or Y axis. Still you will need to do two things
Simple swipe gesture which will trigger the animation.
Pan Gesture which will move the view with movement of the gesture.(Maybe if you drag more than 50% of the label width/height to trigger the rest of the animation.)
Animation will be better if you add gradient from clear colour to the background colour.
Maybe is not the exact answer of your question but it will work good enough.
As addition you can track your pan gesture like this
#objc private func didPan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
//dispalay view
case .changed:
let translation = sender.translation(in: view)
//move view by x or y
case .ended,
.cancelled:
//finish animation or reverted
}
default:
break
}
}
You should have such views
Parent view - this view will clip subview in his bounds
Label - this is your label and it is in the parent
Animation view - it is in the parent and it is outside of it is bounds in the beginning of the animation.
I have a UIView that contains many UIView subviews. When the user tries to pan gesture one of the subviews, I want them to be able to drag it anywhere else on the screen.
However, I can only drag these smaller views within the bigger UIView they are contained in. When the user first tries to pan gesture the smaller view, is there any way to programmatically create a second UIView on top and then drag around the second UIView instead? The second UIView would be completely new from the smaller view that was first touched.
This is the handler function I have so far, just for reference. I don't know where I would programmatically create the second UIView, though. Any help would be greatly appreciated:
#objc func handlePanGesture(sender: UIPanGestureRecognizer) {
// get translation
var translation = sender.translation(in: view)
sender.setTranslation(CGPoint.zero, in: view)
// drag around current UIView
var newView = sender.view as! UIView
newView.center = CGPoint(x: newView.center.x+translation.x, y: newView.center.y+translation.y)
newView.isMultipleTouchEnabled = true
newView.isUserInteractionEnabled = true
if sender.state == UIGestureRecognizer.State.began {
// add something you want to happen when the Label Panning has started
}
if sender.state == UIGestureRecognizer.State.ended {
// add something you want to happen when the Label Panning has ended
}
if sender.state == UIGestureRecognizer.State.changed {
// add something you want to happen when the Label Panning has been change ( during the moving/panning )
} else {
// or something when it's not moving
}
}
Is there any way to be able to drag the smaller views anywhere while maintaining the same layout I have right now
Because of the way touch works, it is impossible by default for a view to be touched when it reaches the boundaries of its superview. But you can overcome that limitation by overriding hitTest in the superview.
A very common pattern that enables dragging of views outside of their superview, is to create a snapshot or clone of the original view as soon as the gesture begins, you can of course hide the original view so to the user it feels like the same UI element is being dragged. It's a pattern used by Apple in their drag and drop sample code here
https://developer.apple.com/documentation/uikit/drag_and_drop/adopting_drag_and_drop_in_a_custom_view
Have a look at line 36 in ViewController+Drag.swift for the relevant code.
What I want to to: I want to drag down the whole view of a viewController to dismiss to the parent viewController using a pan gesture recognizer.
The Problem: When I drag the view down, the navigationBar decreases its height and does not look good. When the view returns to its original position, the navigationBar returns to the default size. I want the navigationBar to stay at its size. I also tried to use the new large titles and some other properties of the navigationController/-bar, but that did not solve it.
Note: Everything worked fine already before iOS 11.
My code:
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(dragViewDown(_:)))
navigationController!.view.addGestureRecognizer(panGesture)
}
#IBAction func dragViewDown(_ gesture: UIPanGestureRecognizer) {
if let dragView = gesture.view {
let translation = gesture.translation(in: dragView)
dragView.center.y = (dragView.center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: dragView)
}
}
This test project only has one viewController and does not provide the dismissal, but the problem is the same as in my working project.
I also uploaded the project to GitHub: https://github.com/maddinK7/navitationBar-pull-down-problem
Does anyone have an idea how to solve this? Thanks in advance.
I want the navigationBar to stay at its size
It is staying at its size. If you check the navigation bar's bounds size height before, during, and after the drag, you will see that it remains the same (probably 44) at all times. What's changing is the drawing extension that causes the drawing of the nav bar to extend up behind the status bar. It can't do that when you pull the whole thing away from the top of the screen, because it is not at the top next to the status bar any more. iOS 11 is more strict about the way it performs this drawing extension, probably because it has to do it in a special way on the iPhone X.
So, let's make sure you're doing this correctly:
Make sure that the navigation bar has a top constraint pinned to the safe area layout guide's top, with a constant of zero.
Make sure that the navigation bar has a delegate that returns .topAttached from position(forBar:).
If you are doing both those things and it doesn't help, you'll have to implement this in some other way entirely. Making the view directly draggable like this, without a custom parent view controller, was always dubious.
When UINavigationController attached top, system will add safe area top margin in the navigation background.
(NOTICE: Background margin will not changed when offset value is between 1 and 0)
So you have to handle attached/detached top event by handle gesture offset to change the right offset and content insets.
You can try the solution in my lib example. ;)
My example include UITableViewController in the UINavigationController, so it will relatively complex.
https://github.com/showang/OverlayModalViewController