I'm creating a kind of grid view where you can add some widgets inside of it and organise them as you want by drag and drop to rearrange them.
My grid view for now is a UIscroll view and the widgets are subclass of UIview.
Here is an example of grid you can have with that
When I drag a widget I wanna be able to make the scroll view go down if the widget is near the bottom of the screen.
For now, my widgets have a UIPanGestureRecognizer
And the following code :
func enterDragMode(recognizer:UIPanGestureRecognizer) {
if (recognizer.state == UIGestureRecognizerState.Ended) || (recognizer.state == UIGestureRecognizerState.Cancelled) {
gridNotificationCenter.postNotificationName("WidgetDragEnded", object: self)
// Notify the grid and drop the widget here
} else {
var translation = recognizer.translationInView(self.superview!)
var newPoint = self.center
newPoint.x += translation.x
newPoint.y += translation.y
if (CGRectContainsRect(moveDownRect, self.frame)) {
var scrollview = self.superview as? UIScrollView
if (scrollview != nil) {
// need to find correct visible rect here
res!.scrollRectToVisible(visibleRect, animated: true)
}
}
recognizer.setTranslation(CGPointZero, inView: self.superview)
}
}
But i feel like I'm not using the right class to handle this properly since the callback is not triggered when I'm simply holding the view and not moving it. Is there a better way ?
Try to set a virtual area near the bottom of your view.
Trigger when the pan gesture is moved. If it enters the area, starts scrolling down as long as the pan gesture does not leave the area and does not end.
And use a timer (NSTimer not CADisplayLink because you can't apply a scroll on the view while you refresh it) to increase the size of your scrollview as long as you are in the specified area.
Related
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.
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.
I want to make my UIView swipeable by X-axis and by Y-axis separately. For example if user swipes view vertically it triggers one action and if user swipes view horizontally it triggers another action. I don't know how to implement this correctly so I'm thinking of attaching two UIPanGestureRecognizers to my view. Is it wrong?
Just use a single UIPanGestureRecognizer and use its translation(in: UIView?) and velocity(in: UIView?) functions to determine which direction the user is swiping.
Don't use two Gesture use only single UIPanGestureRecognizer and call this method with your panGesture:
-(void)moveVerticallyAndHorizentally:(UIPanGestureRecognizer *)gesture{
CGPoint velocity = [gesture velocityInView:self.view]; // you can use your own view
if (fabs(velocity.y) > fabs(velocity.x)) {
// vertical motion
}
else if (fabs(velocity.x) > fabs(velocity.y)){
// Horizental motion
}
}
Hope this will help you.
I have a UIScrollView which is able to contains many view. To allow a good scrolling (without the content going outside the view while scrolling), on my Main.sotryboard , I've clicked on my UIScrollView and then in the attribute inspector I have allowed the Clip Subviews property:
My problem: all the views which are in my UIScrollViews are draggable (because they all have a UIPanGestureRecognizer.
So, when I try to drag them OUTSIDE my UIScrollView, they just disappear.
In fact they're just going behind every other view
To give you an exemple, I have others components which allow the drop of a view form the precedent UIScrollView. So when I begin the drag'n'drop from it, it disappear, and then reappear in the second component on which I have dropped the view.
What I have tried: I have a special UIPanGestureRecognizer for te drag'n'drop of a view coming from this UIScrollView. So, I actually have this (which, obviously, doesn't work, otherwise I would not be here):
//Here recognizer is the `UIPanGestureRecognizer`
//selectpostit is the name of the view I want to drag
if(recognizer.state == UIGestureRecognizerStateBegan){
selectpostit.clipsToBounds = NO;
}
Any Ideas on how I can improve that?
Thanks in advance.
You could try to reset scrollView.clipsToBounds to NO every time gesture starts, but that would lead to side effect when other content outside scroll view would become visible when dragging is in the progress.
I would recommend to take snapshot of the the draggable view when panning starts, place it on the scrollview's parent, and move it. Such approach should solve your problem.
Here is the code:
- (void)onPanGesture:(UIPanGestureRecognizer*)panRecognizer
{
if(panRecognizer.state == UIGestureRecognizerStateBegan)
{
//when gesture recognizer starts, making snapshot of the draggableView and hiding it
//will move shapshot that's placed on the parent of the scroll view
//that helps to prevent cutting by the scroll view bounds
self.draggableViewSnapshot = [self.draggableView snapshotViewAfterScreenUpdates: NO];
self.draggableView.hidden = YES;
[self.scrollView.superview addSubview: self.draggableViewSnapshot];
}
//your code that updates position of the draggable view
//updating snapshot center, by converting coordinates from draggable view
CGPoint snapshotCenter = [self.draggableView.superview convertPoint:self.draggableView.center toView: self.scrollView.superview];
self.draggableViewSnapshot.center = snapshotCenter;
if(panRecognizer.state == UIGestureRecognizerStateEnded ||
panRecognizer.state == UIGestureRecognizerStateCancelled ||
panRecognizer.state == UIGestureRecognizerStateFailed)
{
//when gesture is over, cleaning up the snapshot
//and showing draggable view back
[self.draggableViewSnapshot removeFromSuperview];
self.draggableViewSnapshot = nil;
self.draggableView.hidden = NO;
}
}
I recommend you look at this article by ray wenderlich Moving Table View Cells with a Long Press Gesture
It explains how to create snapshots
This is not strictly a programming question but more "how to accomplish this" question.
I am curious (and working on an app that will probably require this) how is left-right parallax scrolling implemented. To know exactly what I mean check the Yahoo Weather app (it's free - no worries there).
Are they using just one view controller or a separate controller for each view that is shown there?
What is the easiest way to implement this? I have found this topic here which kinda explains it a bit but when are they getting information from their servers? Is it when the views are changed or at the startup of the app?
Any information how to implement such scrolling would be much appreciated.
It's really simple actually:
Subclass UIScrollView
Add a UIView *parallaxView; as #property
Add a CGFloat parallaxFactor as #property
Override layoutSubviews
Call super, and then use self.scrollOffset*parallaxFactor to position the parallaxView
That's it!
I've made a multifunctional UIScrollView subclass myself that's really simple to use, perfect for this case! Get it on GitHub: https://github.com/LeonardPauli/LPParallaxScrollView
Good Luck!
I implemented a small prototype based on a standard UIPageViewController subclass, and autolayout constraints for the parallax effect.
It's quite fully documented, if you want to have a look : TestParallax
In a nutshell, the heart of the parallax is done in the scrollViewDidScroll delegate method:
extension PageViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let screenWidth = scrollView.bounds.width
/*
In a UIPageViewController, the initial contentOffset.x is not 0, but equal to the screen width
(so that the offset goes between (1 * screenWidth) and 0 when going to the previous view controller,
and from (1 * screenWidth) to (2 * screenWidth) when going to the next view controller).
Also, it's reset to the screenWidth when the scroll to a previous or next view controller is complete.
Therefore, we calculate a new 'horizontalOffset' starting at 0, and going:
- negative from 0 to (-screenWidth/2) when scrolling to the next view controller,
- and from 0 to (screenWidth/2) when scrolling to the previous view controller.
*/
let horizontalOffset = (scrollView.contentOffset.x - screenWidth)/2
// Special case: initial situation, or when the horizontalOffset is reset to 0 by the UIPageViewController.
guard horizontalOffset != 0 else {
previousPageController?.offsetBackgroundImage(by: screenWidth/2)
currentPageController?.offsetBackgroundImage(by: 0)
nextPageController?.offsetBackgroundImage(by: -screenWidth/2)
return
}
// The background image of the current page controller should always be offset by the horizontalOffset (which may be positive or negative)
guard let currentPageController = currentPageController else { return }
currentPageController.offsetBackgroundImage(by: horizontalOffset)
if horizontalOffset > 0 { // swiping left, to the next page controller
// The background image of the next page controller starts with an initial offset of (-screenWidth/2), then we apply the (positive) horizontalOffset
if let nextPageController = nextPageController {
let nextOffset = -screenWidth/2 + horizontalOffset
nextPageController.offsetBackgroundImage(by: nextOffset)
}
} else { // swiping right, to the previous page controller
// The background image of the previous page controller starts with an initial offset of (+screenWidth/2), then we apply the (negative) horizontalOffset
if let previousPageController = previousPageController {
let previousOffset = screenWidth/2 + horizontalOffset
previousPageController.offsetBackgroundImage(by: previousOffset)
}
}
}
}
You are not supposed to change the delegate of the page view controller's scroll view. It can break its normal behaviour.
Instead, you can:
Add a pan gesture to the page view controller's view:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panRecognized(gesture:)))
view.addGestureRecognizer(panGesture)
panGesture.delegate = self
Add the new function in order to know how the view is being scrolled.
#objc func panRecognized(gesture: UIPanGestureRecognizer) {
// Do whatever you need with the gesture.translation(in: view)
}
Declare your ViewController as UIGestureRecognizerDelegate.
Implement this function:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}