UIScreenEdgePanGestureRecognizer inside a UIScrollView - ios

I have a horizontal scroll view with 3 views. The first view (far left) is a map view. The functionality I want is that the user can only go to the second view by swiping from the far right edge, because I want other gestures to manipulate the map. But, from the second and third view, they may move left or right by swiping anywhere on the view. I have searched how to do this and have had no luck with my specific situation.
I am not sure where to start so if someone could just point me in the right direction that would be great.
So far I have tried:
disabling scrolling while on the first view and adding in the code:
let edgeRecognizer = UIScreenEdgePanGestureRecognizer(target: firstView, action: "goToSecondView:")
edgeRecognizer.edges = .Right
func goToSecondView(){
scrollView.contentOffset = CGPoint(x: self.scrollView.contentSize.width/3, y: scrollView.contentOffset.y)
}

Really simple just put this Delegate function on your UIScreenEdgePanGestureRecognizer and set the UIGestureRecognizerDelegate
Working with Xcode 8 and Swift 3 :)
You need this 3 delegate functions.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

Instead of using a UIScreenEdgePanGestureRecognizer, let's just make the map view not see touches close to the right edge of the screen. That way the touches will pass through to the scroll view's UIPanGestureRecognizer.
Here's my setup:
(Note: the view frames in the picture do not match the constraints.)
I have a scroll view containing three “page” views. Page 1 (“MapHolder”) contains the map view as a subview. Page 2 (“Pink”) and page 3 (“Green”) just have label subviews, and have a background color to make them easy to see.
The view sizes are constrained as follows:
Pages 2 and 3 are constrained to have the same width and height as the root view (which is the superview of the scroll view). This means that each of them will fill the screen.
Page 1 (“MapHolder”) is constrained to have the same height as the root view, but is constrained to be the width of the root view minus 44.
The map view is constrained to have the same top, bottom, and leading edges as its parent, but to have the same width as the root view.
So notice that the map view is wider than its parent (page 1). The page 1 view has “Clip Subviews” turned off (this is the default), so the part of the map that falls outside of the page 1 view will still be visible, but won't receive touches.
The view positions are constrained as follows:
The scroll view has constraints on all four edges to the root view. This means the scroll view will fill the screen.
Page 1 has constraints from its top, bottom, and leading edges to the scroll view.
Page 2's leading edge is constrained to page 1's trailing edge plus 44.
Page 3's leading edge is constrained to be equal to page 2's trailing edge.
Page 3's trailing edge is constrained to be equal to the scroll view's trailing edge.
The vertical centers of pages 1, 2, and 3 are constrained to be equal.
The constraints from the page views to the scroll view control the scroll view's contentSize.
Finally, I turned on “Paging Enabled” for the scroll view. Result:
I've uploaded my storyboard to this gist. Just put it in a fresh project (replacing the existing Main.storyboard) and link with MapKit to try it out.

Related

How to retain constraints for a Stack View's child after unhiding?

There's a Stack View which contains three labels and has the following constraints:
height = 300
top = Safe Area + 50
trailing/leading = 0
and the following attributes:
Axis -> Vertical
Alignment -> Center
Distribution -> Equal Spacing
Label 3 (blue) has a variation: for Compact height size class Installed attribute is disabled (configured via Attributes Inspector). This makes it hidden in the horizontal orientation on iPhone:
When the app starts all the labels have correct locations on the screen. After rotating to horizontal orientation and back, Label 3 placed in the top left corner of the Stack View while other labels are aligned correctly:
Xcode View Hierarchy debugger reveals that after reappearing Label 3 doesn't have any UIStackView related constraints and the warning next to it says "Position is ambiguous":
It seems that the Label 3 have lost all its constraints related to Stack View after being hidden and shown again.
You can't use the installed attribute for that, since that adds/removes views to the superview. This is not good enough for a StackView, since it requires subviews to be added using addArrangedSubview().
An easy solution is to create an outlet for your label, and hide/show it upon rotation:
#IBOutlet private var label3: UILabel!
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
label3.isHidden = newCollection.verticalSizeClass == .compact
}
A much, much easier method - set trait variations on the Hidden property.
Here is your layout:
Select the bottom label, and in the Attributes Inspector pane, click the + button next to Hidden:
Change the Variation to:
You now have a new Hidden variation which you can select:
and here's what you get when rotated to wC hC:
As these images show, you even see the results in Storyboard... no waiting for code at run-time.

How do i get the vertical distance from an UIView to another one?

As the title suggests, i struggle to find a way to calculate the vertical distance between UIViews.
Say i have two buttons in a View Controller. How would I go about getting a CGFloat indicative of the distance between button1's bottom and button2's top?
As already pointed out by #lobstah, the difference can be calculated by accessing the frames of the views in question.
However, there's an important detail that must be considered: This only works in that simple way, if the views share the same parent view. Because only then, the frames will be expressed with regards to the same coordinate space.
A general approach with doesn't come with this restriction and will always work as long as the views are part of the same view hierarchy (don't appear on different windows) and are not scaled / rotated by an affine transform is the following:
func distanceBetween(bottomOf view1: UIView, andTopOf view2: UIView) -> CGFloat {
let frame2 = view1.convert(view2.bounds, from: view2)
return frame2.minY - view1.bounds.maxY
}
The distance would be positive if the top of view2 is below the bottom of view1.
You can use maxY of the top and minY of bottom view’s frames properties:
bottomView.frame.minY - topView.frame.maxY

How to scroll UICollectionView that is underneath another UICollectionView?

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 :)

ScrollView - Gesture Recognizer - Swipe vertically

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

Issue with horizontal scrollview inside a vertical scrollview in ios swift

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.

Resources