UIScrollView pan with 1 finger when zoomed in - swift

I got a scrollView with some sub views added to it. Scrolling and zooming works fine.
What I would like to is when the scrollView is zoomed in, alter the behavior of the 1 finger touch to pan instead of scroll.
Right now if you are zoomed in and move around with 1 finger you can only scroll the zoomed in content. With two fingers you can pan around. This is the behaviour I would like to change.
I haven't found any solution to this issue and I don't really have any ideas except trying to alter the minimum and maximum touches required but it doesn't seem to work.
func scrollViewDidZoom(_ scrollView: UIScrollView) {
scrollView.panGestureRecognizer.maximumNumberOfTouches = 1
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
if scale == 1 {
scrollView.panGestureRecognizer.maximumNumberOfTouches = 2
}
}

I realized the issue I had was that each subview had their own scrollView (in my case my subviews wkWebViews). They were handling their own zoom even though I was trying to zoom from the UIScrollView.
I took these steps to solve this issue.
MyClass: UIViewController, UIScrollViewDelegate {
// This view should handle the zooming
#IBOutlet var scrollView: UIScrollView!
// .. and this view will contain all subviews to be zoomed
#IBOutlet var wrapper: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.scrollView.delegate = self
self.addSViewsToScrollView()
}
func addViewsToScrollView () {
// MARK: - Step 1
// .. set wkWebView scrollview maximum and minimum zoom scale to 1.
let myArray: [WkWebView]? = [WkWebView(),WkWebView(),WkWebView()]
for view in myArray {
wkWebView.scrollView.maximumZoomScale = 1
wkWebView.scrollView.minimumZoomScale = 1
wkWebView.isUserInteractionEnabled = false // This is key otherwise it wont work
wkWebView.scrollView.isUserInteractionEnabled = false // .. this aswell
}
// MARK: - Step 2
// .. add all views to a wrapper view
for view in myArray {
self.wrapperView.addSubview(view)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.wrapper
}
}
By returning a single view the panning in the zoomed in state works fine. Also the wkWebView scrollViews do not interfere with the zooming anymore.

Related

Touch Gesture on image is not working in ScrollView? - Swift

This is the image on the scroll view where I'm able create those rectangles from superview
This is the image I want how to look my output when I touch to began creating multiple rectangles
I'm trying to create multiple annotations on images using views where it's been achieved. And for the next step trying to zoom in through image view to annotate where it's possible with pinch gesture but the constraint is not able scroll left or right while zoomed in. I have to zoom out and then again have to zoom in to the concentrated area or co-ordinate to annotate again. I gone through few examples and found to be possible of moving or scrolling left or right while zoomed in using scroll view, where I have achieved it. But not able to annotate the image by creating views. When I tried to create it's not happening still accidently I srolled it from superview where it has created the view. Thank you in adavnce.
Class ViewController : UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate{
// Outlets
#IBOutlet weak var displayImageView : UIImageView!
#IBOutlet weak var imageScrolling : UIScrollView!
#IBOutlet weak var containerView : UIView!
// View Did Load
override func viewDidLoad() {
super.viewDidLoad()
displayImageView.isMultipleTouchEnabled [](https://i.stack.imgur.com/ba2n7.png)= false
displayImageView.isUserInteractionEnabled = true
imageScrolling.minimumZoomScale = 1.0
imageScrolling.maximumZoomScale = 4.0
imageScrolling.delegate = self
imageScrolling.contentSize = displayImageView.bounds.size
imageScrolling.isUserInteractionEnabled = true
containerView.isUserInteractionEnabled = true
imageScrolling.isExclusiveTouch = true
imageScrolling.panGestureRecognizer.isEnabled = true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// View For Zooming - Scroll View Image Zooming and Scrolling
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return displayImageView
}
}
I have also added the screenshot of how it's happening while touch began from superview and how i wanted it to be.
I have also added views hierarchy

how to have UITableView's pinned headers hide after scrolling

I want to have my tableViewHeaders visible as the user scrolls by pinning to the top which is the current behaviour in my tableView. However, when the tableView stops scrolling, I want to remove these 'pinned' headers. I am achieving this in my collectionView project using the following in my scrollView delegate methods:
if let cvl = chatCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
cvl.sectionHeadersPinToVisibleBounds = false
cvl.invalidateLayout()
}
Is there a similar way to hide a tableView's 'pinned' (sticky) headers? I am using a tableViewController.
This is my solution to this issue. I wonder if there is a simpler way to do this though.
Please note, this will only work if your header is a UITableViewHeaderFooterView. Not if you are using a UITableViewCell for a header. If you are using a UITableViewCell, tableView.headerView(forSection: indexPathForVisibleRow.section) will return nil.
In order to hide the pinned headers when the tableView stops scrolling and have them re-appear when the tableView starts scrolling again, override these four scrollView delegate methods.
In the first two (scrollViewWillBeginDragging and scrollViewWillBeginDecelerating), get the section header for the first section of the visible rows and make sure it is not hidden.
In the second two delegate methods, do a check to see that for each of the visible rows, the header frame for that row is not overlapping the frame for the row cell. If it is, then this is a pinned header and we hide it after a delay. We need to ensure that the scrollView is not still dragging before removing the pinned header as will be the case when the user lifts their finger but the scroll view continues to scroll. Also because of the time delay, we check that the scrollView is not dragging before removing it in case the user starts scrolling again less than 0.5 seconds after the scroll stops.
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
showPinnedHeaders()
}
override func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
showPinnedHeaders()
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
removePinnedHeaders()
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
removePinnedHeaders()
}
private func showPinnedHeaders() {
for section in 0..<totalNumberOfSectionsInYourTableView {
tableView.headerView(forSection: section)?.isHidden = false
}
}
private func removePinnedHeaders() {
if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows {
if indexPathsForVisibleRows.count > 0 {
for indexPathForVisibleRow in indexPathsForVisibleRows {
if let header = tableView.headerView(forSection: indexPathForVisibleRow.section) {
if let cell = tableView.cellForRow(at: indexPathForVisibleRow) {
if header.frame.intersects(cell.frame) {
let seconds = 0.5
let delay = seconds * Double(NSEC_PER_SEC)
let dispatchTime = DispatchTime.now() + Double(Int64(delay)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: {
if !self.tableView.isDragging && header.frame.intersects(cell.frame) {
header.isHidden = true
}
})
}
}
}
}
}
}
}
Additionally add removePinnedHeaders() to viewDidAppear() and any other rotation or keyboard frame change methods that will scroll your tableView.

make UIView in UIScrollView stick to the top when scrolled up

So in a UITableView when you have sections the section view sticks to the top until the next section overlaps it and then it replaces it on top. I want to have a similar effect, where basically I have a UIView in my UIScrollView, representing the sections UIView and when it hits the top.. I want it to stay in there and not get carried up. How do I do this? I think this needs to be done in either layoutSubviews or scrollViewDidScroll and do a manipulation on the UIVIew..
To create UIView in UIScrollView stick to the top when scrolled up do:
func createHeaderView(_ headerView: UIView?) {
self.headerView = headerView
headerViewInitialY = self.headerView.frame.origin.y
scrollView.addSubview(self.headerView)
scrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let headerFrame = headerView.frame
headerFrame.origin.y = CGFloat(max(headerViewInitialY, scrollView.contentOffset.y))
headerView.frame = headerFrame
}
Swift Solution based on EVYA's response:
var navigationBarOriginalOffset : CGFloat?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationBarOriginalOffset = navigationBar.frame.origin.y
}
func scrollViewDidScroll(scrollView: UIScrollView) {
navigationBar.frame.origin.y = max(navigationBarOriginalOffset!, scrollView.contentOffset.y)
}
If I recall correctly, the 2010 WWDC ScrollView presentation discusses precisely how to keep a view in a fixed position while other elements scroll around it. Watch the video and you should have a clear-cut approach to implement.
It's essentially updating frames based on scrollViewDidScroll callbacks (although memory is a bit hazy on the finer points).
Evya's solution works really well, however if you use Auto Layout, you should do something like this (The Auto Layout syntax is written in Masonry, but you get the idea.):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//Make the header view sticky to the top.
[self.headerView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_top).with.offset(scrollView.contentOffset.y);
make.left.equalTo(self.scrollView.mas_left);
make.right.equalTo(self.scrollView.mas_right);
make.height.equalTo(#(headerViewHeight));
}];
[self.scrollView bringSubviewToFront:self.headerView];
}

check if UIView is in UIScrollView visible state

What is the easiest and most elegant way to check if a UIView is visible on the current UIScrollView's contentView? There are two ways to do this, one is involving the contentOffset.y position of the UIScrollView and the other way is to convert the rect area?
If you're trying to work out if a view has been scrolled on screen, try this:
CGRect thePosition = myView.frame;
CGRect container = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.frame.size.width, scrollView.frame.size.height);
if(CGRectIntersectsRect(thePosition, container))
{
// This view has been scrolled on screen
}
Swift 5: in case that you want to trigger an event that checks that the entire UIView is visible in the scroll view:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(targetView.frame) {
// entire UIView is visible in scroll view
}
}
}
Implement scrollViewDidScroll: in your scroll view delegate and calculate manually which views are visible (e.g. by checking if CGRectIntersectsRect(scrollView.bounds, subview.frame) returns true.
updated for swift 3
var rect1: CGRect!
// initialize rect1 to the relevant subview
if rect1.frame.intersects(CGRect(origin: scrollView.contentOffset, size: scrollView.frame.size)) {
// the view is visible
}
I think your ideas are correct. if it was me i would do it as following:
//scrollView is the main scroll view
//mainview is scrollview.superview
//view is the view inside the scroll view
CGRect viewRect = view.frame;
CGRect mainRect = mainView.frame;
if(CGRectIntersectsRect(mainRect, viewRect))
{
//view is visible
}
José's solution didn't quite work for me, it was detecting my view before it came on screen. The following intersects code works perfect in my tableview if José's simpler solution doesn't work for you.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
Solution that takes into account insets
public extension UIScrollView {
/// Returns `adjustedContentInset` on iOS >= 11 and `contentInset` on iOS < 11.
var fullContentInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return adjustedContentInset
} else {
return contentInset
}
}
/// Visible content frame. Equal to bounds without insets.
var visibleContentFrame: CGRect {
bounds.inset(by: fullContentInsets)
}
}
if scrollView.visibleContentFrame.contains(view) {
// View is fully visible even if there are overlaying views
}

Making two UIScrollViews follow each others scrolling

How would I make two scroll views follow each others scrolling?
For instance, I have a scroll view (A) on the left of a screen, whose contents can scroll up and down, but not left and right. Scroll view B matches the up and down of A, but can also scroll left and right. Scroll view A is always on the screen.
-----------------------------------------------------------
| | |
| | |
| | |
| A | B |
| | |
| scrolls | |
| up & down | scrolls all directions |
| | |
-----------------------------------------------------------
How would I make it so the the up and down scrolling (of either view) also makes the other view scroll in the same up-down direction? Or is there another method to do this?
Set the delegate of scroll view A to be your view controller... then have...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
}
If you want both to follow each other, then set delegate for both of them and use...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
} else {
CGPoint offset = scrollViewA.contentOffset;
offset.y = scrollViewB.contentOffset.y;
[scrollViewA setContentOffset:offset];
}
}
The above can be refactored to have a method which takes in two scrollviews and matches one to the other.
- (void)matchScrollView:(UIScrollView *)first toScrollView:(UIScrollView *)second {
CGPoint offset = first.contentOffset;
offset.y = second.contentOffset.y;
[first setContentOffset:offset];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
[self matchScrollView:scrollViewB toScrollView:scrollViewA];
} else {
[self matchScrollView:scrollViewA toScrollView:scrollViewB];
}
}
Swift 3 Version:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == scrollViewA {
self.synchronizeScrollView(scrollViewB, toScrollView: scrollViewA)
}
else if scrollView == scrollViewB {
self.synchronizeScrollView(scrollViewA, toScrollView: scrollViewB)
}
}
func synchronizeScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
var offset = scrollViewToScroll.contentOffset
offset.y = scrolledView.contentOffset.y
scrollViewToScroll.setContentOffset(offset, animated: false)
}
I tried the Simon Lee's answer on iOS 11. It worked but not very well. The two scroll views was synchronized, but using his method, the scroll views would lost the inertia effect(when it continue to scroll after you release your finger) and the bouncing effect. I think it was due to the fact that setting the contentOffset through setContentOffset(offset, animated: false) method causes cyclic calls of the scrollViewDidScroll(_ scrollView: UIScrollView) delegate's method(see this question)
Here is the solution that worked for me on iOS 11:
// implement UIScrollViewDelegate method
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollViewA {
self.syncScrollView(self.scrollViewB, toScrollView: self.scrollViewA)
}
else if scrollView == self.scrollViewB {
self.syncScrollView(self.scrollViewA, toScrollView: scrollViewB)
}
}
func syncScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
var scrollBounds = scrollViewToScroll.bounds
scrollBounds.origin.y = scrolledView.contentOffset.y
scrollViewToScroll.bounds = scrollBounds
}
So instead of setting contentOffset we are using bounds property to sync the other scrollView with the one that was scrolled by the user. This way the delegate method scrollViewDidScroll(_ scrollView: UIScrollView) is not called cyclically and the scrolling happens very smooth and with inertia and bouncing effects as with a single scroll view.
Swift 5.4 // Xcode 13.1
What has worked flawlessly for me was the following:
Create a custom subclass of UIScrollView
Conform to UIGestureRecognizer delegate
Override the gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) GestureRecognizerDelegate method
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Both collection views need to have the same superview. Add both gesture recognizers to your superview
yourSuperview.addGestureRecognizer(scrollView1.panGestureRecognizer)
yourSuperview.addGestureRecognizer(scrollView2.panGestureRecognizer)
Hope this helps!
Guys i know the question is answered, but decided to share with you here my approach for solving a similar issue that i had, because i believe it is a pretty clean solution. Basically i had to make three collection views scroll together, and what i did, is i made a custom UICollectionView subclass called SharedOffsetCollectionView, that when you set this class to a collection view in storyboards or you instantiate it from code directly, you can have all instances scroll the same.
So with SharedOffsetCollectionView all collection view instances of this class in your app, will scroll the same always. In my opinion it is a clean solution because it requires adding zero logic in your view controllers, it is all contained in this external class, you just have to set the class of your collection view to be SharedOffsetCollectionView and you are done.
The same approach could easily be transferred to UITableViews and UIScrollViews
Hope that is helpful to some you. :)
My solution is written in:
Swift 5.2, XCode 11.4.1
The answer above all did not quite work our for me, since I run into a cyclic call of scrollViewDidScroll. This happend, because setting the content offset of a scroll view also calls scrollViewDidScoll. I solved it by putting a lock between it, which is set based on if a scroll view is being dragged by the user or not, so the syncing won't happen by setting the content offset:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.synchronizeScrollView(offSet: scrollView.contentOffset.y,
scrollViewAIsScrolling: scrollView == scrollViewA,
isScroller: scrollView.isDragging)
}
private enum Scroller {
case page, time
}
private var scroller: Scroller? = nil
func synchronizeScrollView(offSet: CGFloat, scrollViewAIsScrolling: Bool,
isScroller: scrollView.isDragging) {
let scrollViewToScroll = scrollViewAIsScrolling ? scrollViewB : scrollViewA
var offset = scrollViewToScroll.contentOffset
offset.y = offSet
scrollViewToScroll.setContentOffset(offset, animated: false)
}
This code can be refactored based on how many scroll views are used and based on who owns them. I won't recommend having one controller being the delegate of many scroll views. I would rather solve it with delegation.