I have a UIScrollView that works when swiping left or right, however I've reduced the size of the scrollView so, now display area doesn't fully occupy the superview's frame, and swiping works only within the frame of the scroll view.
I would like to be able to scroll vertically even when swiping up and down outside the horizontal bounds of the narrowed scroll view.
It was recommended that I use a gesture recognizer, but that's beyond my current familiarity with iOS and could use more specific advice or a bit more guidance to get started with that.
There is a simpler approach then use a Gesture Recognizer =]
You can setup the superview of the scroll view (which is BIGGER...) to pass the touches to the scroll view. It's working M-A-G-I-C-A-L-Y =]
First, select the view that will pass all it's touches to the scroll view. if your parent view is already ok with that you may use it. otherwise you should consider add a new view in the size that you want that will catch touches.
Now create a new class (I'll use swift for the example)
class TestView: UIView {
#IBOutlet weak var Scroller: UIScrollView!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if (view == self) {
return Scroller
}
return view
}
}
Nice! now as you can see we added an outlet of the scroller. so use interface builder, select the new view and set it's class to "TestView" in the identity inspector (Or to the name that you'll use for your custom class).
After you set the class and your view is still selected go to connections inspector and connect "Scroller" to your scroll view on the storyboard. All connected properly =]
That's it!! no gesture recognizer needed!!
The new view will pass all it's touches to the scroll view and it'll behave just like you pan in it =]
In my answer I used that answer
EDIT: I improved the code now, it wasn't working as expected before, now it catches only when in needs and not every touch in the app as before
Search for a component called SwipeGestureRecognizer :
Grab it and drop it on top of the View (use the hierarchy to make sure
you drop it on it, if you drop it on another element this code will not work):
Select one of the SwipeGestureRecognizer in the hierarchy and go to its attribute page. Change Swipe to Right.
Make sure the other recogniser has the Swipe attribute to Left
Select UIScrollView and uncheck Scrolling enabled
Connect detectSwipe() (see source code below) to both recognizers.
--
#IBAction func detectSwipe (_ sender: UISwipeGestureRecognizer) {
if (currentPage < MAX_PAGE && sender.direction == UISwipeGestureRecognizerDirection.left) {
moveScrollView(direction: 1)
}
if (currentPage > MIN_PAGE && sender.direction == UISwipeGestureRecognizerDirection.right) {
moveScrollView(direction: -1)
}
}
func moveScrollView(direction: Int) {
currentPage = currentPage + direction
let point: CGPoint = CGPoint(x: scrollView.frame.size.width * CGFloat(currentPage), y: 0.0)
scrollView.setContentOffset(point, animated: true)
// Create a animation to increase the actual icon on screen
UIView.animate(withDuration: 0.4) {
self.images[self.currentPage].transform = CGAffineTransform.init(scaleX: 1.4, y: 1.4)
for x in 0 ..< self.images.count {
if (x != self.currentPage) {
self.images[x].transform = CGAffineTransform.identity
}
}
}
}
Refer to https://github.com/alxsnchez/scrollViewSwipeGestureRecognizer for more
I don't have time for detailed answer but:
In storyboard drag a pan gesture recognizer on the scroll view's superview... Connect it's action with your view controller and in this action change the scroll view position by using the properties from the gesture recognizer that you got as parameter
Tip: when connecting the action change parameter type from Any to UIPanGestureRecognizer in the combo box
please don't see this answer as recommendation to use this approach in your problem, I don't know if that's the best way, I'm just helping you to try it
Related
Here is an image of a simple APP I am trying to make
The App has a pan gesture recogniser for the centre purple button, the button when moved if it intersects with any of the 4 orange buttons, the purple button animates and moves inside the bounds of button it intersects otherwise it animates back to the initial starting position i.e centre.
For the first time when I pan the purple button the frame of the button updates, but If I try for the second time the frame of button remains the same (the value when it's at the centre of the view).
I am guessing this is something related to Auto Layout that I am missing, because if I remove the constrains on the centre purple button the frame updates every time correctly if I pan.
Can anybody explain what I have to keep in mind when animating with constraints
Here is my code for handling the Pan gesture:
#objc func handleCenterRectPan(_ pannedView: UIPanGestureRecognizer) {
let fallBackAnimation = UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.5) {
if self.button1.frame.intersects(self.centerRect.frame) {
self.centerRect.center = self.button1.center
} else if self.button2.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button2.center
} else if self.button3.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button3.center
} else if self.button4.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button4.center
} else {
// no intersection move to original position
self.centerRect.frame = OGValues.oGCenter
}
}
switch pannedView.state {
case .began:
self.centerRect.center = pannedView.location(in: self.view)
case .changed:
print("Changed")
self.centerRect.center = pannedView.location(in: self.view)
case .ended, .cancelled, .failed :
print("Ended")
fallBackAnimation.startAnimation();
//snapTheRectangle()
default:
break
}
}
First of all, you shouldn't be giving constraints if your aim is to move your views smoothly. And If you can't resist from constraints than you have to keep updating constraints and not the frames (centre),
In your code, I assume you have given your purple button centre constraints and then you are changing button's centre as user drag using the pan gesture. The problem is constraints you have given are still active and its try to set it back as soon as layout needs to update.
So what you can do is
Don't give constraints as its need to move freely when user drag
OR
Create IBoutlet for constraints and set .isActive to false when user dragging and make it true if it's not inside any of the other buttons and update UpdateLayoutIfNeeded, so it will come back to original position. (good approach)
#IBOutlet var horizontalConstraints: NSLayoutConstraint!
#IBOutlet var verticalConstraints: NSLayoutConstraint!
//make active false as the user is dragging the view
horizontalConstraints.isActive = false
verticalConstraints.isActive = false
self.view.layoutIfNeeded()
//make it true again if its not inside any of the other views
horizontalConstraints.isActive = true
verticalConstraints.isActive = true
self.view.layoutIfNeeded()
OR
Update constraints.constants for the horizontal and vertical constraints when user is moving the view (Not a good approach) and make it zero again if it's not inside of any other view
//make it true again if its not inside any of the other views
horizontalConstraints.constant = change in horizontal direction
verticalConstraints..constant = change in vertical direction
self.view.layoutIfNeeded()
Edit As you said constraints is nil after setting isActive = false because
what isActive actually do is add and remove those constraints as Apple doc says
Activating or deactivating the constraint calls addConstraint(:) and removeConstraint(:) on the view that is the closest common ancestor of the items managed by this constraint. Use this property instead of calling addConstraint(:) or removeConstraint(:) directly.
So making constraints strong reference does solve the issue but I think setting the strong reference to IBOutelet is not a good idea and adding and removing constraints programmatically is better. check out this question deallocating constraints
I'm having trouble accepting two gestures simultaneously across my sibling views. The view structure is as follows.
Superview
|
|--> ChildView1 (UITableView)
|
|--> ChildView2 (UIView).. Partially overlaps ChildView1
When I do a pan gesture on ChildView2, I would like that to pass through to ChildView1 so that the UITableView scrolls properly.
However, when I do a LongPress gesture on ChildView2, I would like that to be properly recognized within ChildView2.
The closest question (and answer) I've seen is this. However, unlike that question, where ChildView1 has to handle the passed gesture, I would like the UITableView to handle the gesture and scroll as if it was scrolled directly on the view. Is that possible?
Thanks for any insights.
Create a subclass of UIView and add it as the class for childView2
class customView:UIView
{
override func point(inside point: CGPoint,with event: UIEvent?) -> Bool
{
return false
}
}
So heres my issue, the 4 orange rectangles you see on the gif are a single vertical UICollectionView = orangeCollectionView.
The Green and Purple "card" views are part of another UICollectionView = overlayCollectionView.
overlayCollectionView has 3 cells, one of which is just a blank UICollectionViewCell, the other 2 are the cards.
When the overlayCollectionView is showing the blank UICollectionViewCell, I want to be able to scroll the orangeCollectionView.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let superr = superview else { return true}
for view in superr.subviews {
if view.isKind(of: OrangeCollectionView.self) {
view.point(inside: point, with: event)
return false
}
}
return true
}
This allows me to scroll the orangeCollectionView HOWEVER this doesn't actually work to fix my issue. I need to be able to scroll left and right to show the cards, however this blocks all touches becuase the point always falls on the OrangeCollectionView.
How can I check to see if they are scrolling left/right to show the cards? Otherwise if they are on the blank cell, scroll the orangeViewController up and down.
Had this issue as well... Didn't find a nice way to do it, but this works.
First, you need to access the scroll view delegate method scrollViewDidScroll(). There you call:
if scrollView == overlayScrollView {
if scrollView.contentOffset.x == self.view.frame.width { // don't know which coordinate you need
self.overlayScrollView.alpa = 0
}
}
After that, you add a blank view onto of the orange collection view. The view's alpha is 0 (I think, maybe the color is just clear; try it out if it works).
In the code, you then add a UISwipeGestureRecognizer to the view you just created and and detect whether there's a swipe to the left or to the right.
By detecting the direction of that swipe, you can simply change the contentOffset.x axis of your overlayScrollView to 0 or self.view.frame.width * 2.
Sorry I can't provide my working sample code, I'm answering from my mobile. It's not the proper solution, but when I made a food app for a big client it worked perfectly and no one ever complained :)
I am trying to figure out a way to drag and delete a subview when reaches at a certain position. What i have so far is a view controller on which i add several subviews. My subview classes are rotatable and resizable and i can move them on the view controller. Let say that i want to move a subview on top center of my view controller and delete it. How my parent view controller know the position of the subview and perform the deletion?
Another issue is that all the subviews are generated from the same custom class. How should i define which subview is the one that should be deleted?
Thank you in advance.
You can use a UIPanGestureRecognizer to move the views and CGRect.intersects to see if the new position takes you into the delete zone (use instead contains if you want to require that you are completely inside the delete zone):
#IBAction func pan(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
centerXConstraint.constant += translation.x
centerYConstraint.constant += translation.y
viewToMove.offset.x += translation.x
viewToMove.offset.y += translation.y
if viewToMove.frame.intersects(deletionRect) {
//Delete logic here
} else {
viewToMove.setNeedsDisplay()
recognizer.setTranslation(.zero, in: self.view)
}
}
If you have multiple views you can use either UIView.tag or a Set to figure out which view is which.
My layout is currently like this:
View
-- View
-- Vertical ScrollView
------ View
--------- Horizontal Paginated ScrollView
--------- View
------------- Horizontal ScrollView -- not working properly
See this image for view hierarchy screenshot from xcode:
Using Swift.
I am adding subviews dynamically to this "Size Select Scroll View"
Two Issues:
After adding views, there is no margin between the subviews. Each subview's coord. are like this: (10.0, 0.0, 44.0, 44.0), (54.0, 0.0, 44.0, 44.0), (98.0, 0.0, 44.0, 44.0), (142.0, 0.0, 44.0, 44.0) etc.
But the appearance is like this without the 10 points gap between each subview: http://i.stack.imgur.com/mCGRW.png
Scrolling horizontally is a pain. It only works on maybe 1/4 height from top of the scrollview area and very difficult to scroll. How do i layout subviews so that this scrollview is properly scrollable?
Note: I am explicitly setting content size of size scrollview to more than required so that i can see the scrolling.
I found out the issue. According to the Apple Docs the touch events will be passed to a subview only if it lies entirely in its parent.
In my case, the scrollview was going out of bounds of its parent view ( Details View), because of which touch events were weird. I increased the parent view's size to fit the scrollview and it works fine now.
From the docs (https://developer.apple.com/library/content/qa/qa2013/qa1812.html):
The most common cause of this problem is because your view is located outside the bounds of its parent view. When your application receives a touch event, a hit-testing process is initiated to determine which view should receive the event. The process starts with the root of the view hierarchy, typically the application's window, and searches through the subviews in front to back order until it finds the frontmost view under the touch. That view becomes the hit-test view and receives the touch event. Each view involved in this process first tests if the event location is within its bounds. Only after the test is successful, does the view pass the event to the subviews for further hit-testing. So if your view is under the touch but located outside its parent view's bounds, the parent view will fail to test the event location and won't pass the touch event to your view.
This is always a challenge in iOS.
There are various solutions which unfortunately depend on the exact situation.
Here's a drop-in solution which is often the right solution.
/*
So, PASS any touch to the NEXT view, BUT ALSO if any of OUR
subviews are buttons, etc, then THOSE should ALSO work normally. :/
*/
import UIKit
class Passthrough: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
}
(Of course, you can also just drop the call in to some class, eg
class SomeListOrWhatever: UICollectionView, .. {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("MIGHT AS WELL TRY THIS")
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
Even if you "don't totally understand what the problem is", this is "one of" the solutions!
For example, this is (usually!) the precise solution to the exact issue quoted from the doco by #kishorer747
It's definitely a real nuisance in iOS.