I have two views; a UIView placed on top of a UITableView. I need to know when the UIView has been panned, so I’ve placed a UIPanGestureRecognizer on it. However, this creates a UI which seems buggy because you expect the UITableView behind it to move as your finger does.
So it seems I need to somehow pair up this pan gesture with making the table view behind it move, at least, until this covering view disappears.
How do I pair up a pan gesture to move a UIScrollView?
Note: If you’re wondering about the cover view, it’s actually a UIImageView which has a snapshot of the table view with a filter applied to it for UI reasons.
When this view is panned, it disappears. So from the user’s point of view, while they begin dragging the cover view, I want them to keep thinking they are dragging the table view as the cover disappears.
Assuming you have a view controller with a table view setup like the following:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate
{
#IBOutlet var tableView: UITableView!
#IBOutlet var scrollView: UIScrollView!
You can add a couple of gesture recognizers:
var gestureRecognizer1: UIPanGestureRecognizer = UIPanGestureRecognizer()
var gestureRecognizer2: UIPanGestureRecognizer = UIPanGestureRecognizer()
Setup your delegates and add the gesture recognizers to the scroll views:
override func viewDidLoad()
{
super.viewDidLoad()
scrollView.contentSize = CGSizeMake( ) // <-- Set your content size.
scrollView.delegate = self
tableView.delegate = self
gestureRecognizer1.delegate = self
gestureRecognizer2.delegate = self
tableView.addGestureRecognizer(gestureRecognizer1)
scrollView.addGestureRecognizer(gestureRecognizer2)
}
Implement a delegate method for UIGestureRecognizerDelegate to recognize simultaneous gestures:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
Implement a delegate method for UIScrollViewDelegate:
func scrollViewDidScroll(scrollView: UIScrollView)
{
tableView.contentOffset = scrollView.contentOffset
}
}
The movement of the tableView will be synchronized to the scroll view.
In the end I’ve decided to directly change the cell’s images with a CIFilter rather than take a snapshot. This way is the way I should have done it anyway, it’s much cleaner and uses no possibly-buggy “workarounds”.
Related
I am looking for a way to override what happens when a user does an accessibility scroll (three finger scroll with VoiceOver enabled) to the left or right in a WKWebView on iOS. I tried subclassing WKWebView and overriding goBack(), goForward(), and accessibilityScroll(...). My subclass methods do not get called. I also tried implementing accessibilityScroll(...) in my view controller, but that method did not get called either.
Ideally I want to continue letting WKWebView handle three finger scrolls up and down, but add my own behavior when a user three finger scrolls to the left or right.
You could try capturing the accessibility scroll of the WebViews scrollView
class ViewController: UIViewController {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView.scrollView.delegate = self
}
}
extension ViewController: UIScrollViewDelegate {
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
// Your code here
}
}
I'm trying to put a UIView (banner) on top of the list (tableView), so the UIview will not disappear when the user scrolls down the list.
I tried this code, but didn't work.
override func viewDidLayoutSubviews() {
self.view.addSubview(banner)
banner.frame.size.width = self.view.frame.size.width
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
var rect = self.banner.frame
rect.origin.y = max(0,scrollView.contentOffset.y + scrollView.contentInset.top)
self.banner.frame = rect
}
Any advice how to fix it? Thank you
Instead of trying to keep the view within the scrollView, a much easier method would be to use a standard UIViewController with a UIView attached to the top using AutoLayout with constraints and UITableView attached to the bottom of the UIView and the bottom of the UIViewController.
With this layout, only your smaller tableView would be scrolling and your UIView would be stationary in place, outside the scope of the scrolling UITableViewCells.
If you are currently using an UITableViewController, you'll need to remove the override modifiers from your tableView methods, make your UIViewController implement UITableViewDataSource and UITableViewDelegate, then attach the delegates either in code or storyboard.
Your new UIViewController would look like this at the top:
class MyController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
}
The problem in short, related to working with pan gesture inside a scrollView.
I have a canvas(which is an UIView itself but bigger in size) where i am drawing some UIView objects with pan gesture enabled over each of them(Each little UIView Objects I am talking about, are making using another UIView class).
Now the canvas can be bigger in height and width...which can be changed as per the user input.
So to achieve that I have placed the canvas inside a UIScrollView. Now the canvas is increasing or decreasing smoothly.
Those tiny UIView objects on the canvas can be rotated also.
Now the problem.
If I am not changing the canvas size(static) i.e. if its not inside the scrollview then each UIView objects inside the canvas are moving superbly and everything is working just fine with the following code.
If the canvas is inside the UIScrollView then the canvas can be scrollable right? Now inside the scrollview if I am panning the UIView objects on the canvas then those little UIView objects are not following the touch of the finger rather than its moving on another point when touch is moving on the canvas.
N.B. - Somehow I figured out that I need to disable the scrolling of the scrollview when any of the subviews are getting touch. For that thing I have implemented NSNotificationCenter to pass the signal to the parent viewController.
Here is the code.
This functions are defined inside the parent viewController class
func canvusScrollDisable(){
print("Scrolling Off")
self.scrollViewForCanvus.scrollEnabled = false
}
func canvusScrollEnable(){
print("Scrolling On")
self.scrollViewForCanvus.scrollEnabled = true
}
override func viewDidLoad() {
super.viewDidLoad()
notificationUpdate.addObserver(self, selector: "canvusScrollEnable", name: "EnableScroll", object: nil)
notificationUpdate.addObserver(self, selector: "canvusScrollDisable", name: "DisableScroll", object: nil)
}
This is the Subview class of the canvas
import UIKit
class ViewClassForUIView: UIView {
let notification: NSNotificationCenter = NSNotificationCenter.defaultCenter()
var lastLocation: CGPoint = CGPointMake(0, 0)
var lastOrigin = CGPoint()
var myFrame = CGRect()
var location = CGPoint(x: 0, y: 0)
var degreeOfThisView = CGFloat()
override init(frame: CGRect){
super.init(frame: frame)
let panRecognizer = UIPanGestureRecognizer(target: self, action: "detectPan:")
self.backgroundColor = addTableUpperViewBtnColor
self.multipleTouchEnabled = false
self.exclusiveTouch = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func detectPan(recognizer: UIPanGestureRecognizer){
let translation = recognizer.translationInView(self.superview!)
self.center = CGPointMake(lastLocation.x + translation.x, lastLocation.y + translation.y)
switch(recognizer.state){
case .Began:
break
case .Changed:
break
case .Ended:
notification.postNotificationName("EnableScroll", object: nil)
default: break
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
notification.postNotificationName("DisableScroll", object: nil)
self.superview?.bringSubviewToFront(self)
lastLocation = self.center
lastOrigin = self.frame.origin
let radians:Double = atan2( Double(self.transform.b), Double(self.transform.a))
self.degreeOfThisView = CGFloat(radians) * (CGFloat(180) / CGFloat(M_PI) )
if self.degreeOfThisView != 0.0{
self.transform = CGAffineTransformIdentity
self.lastOrigin = self.frame.origin
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4))
}
myFrame = self.frame
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
notification.postNotificationName("EnableScroll", object: nil)
}
}
Now the scrollView is disabling its scroll perfectly whenever one of the UIView object is receiving touch over the canvas which is inside the scrollview but sometimes those UIView objects are not properly following the touch location over the canvas/screen.
I am using Swift 2.1 with Xcode 7 but anyone can tell me the missing things of mine or the solution using Objective-c/Swift?
Where do you set the lastLocation? I think it would be better for you to use locationInView and compute the translation by yourself. Then save the lastLocation on every event that triggers the method.
Also you might want to handle the Cancel state as well to turn the scrolling back on.
All of this does seem a bit messy though. The notifications are maybe not the best idea in your case nor is putting the gesture recognizers on the subviews. I think you should have a view which handles all those small views; it should also have a gesture recognizer that can simultaneously interact with other recognizers. When the gesture is recognized it should check if any of the subviews are hit and decide if any of them should be moved. If it should be moved then use the delegate to report that the scrolling must be disabled. If not then cancel the recognizer (disable+enable does that). Also in most cases where you put something movable on the scrollview you usually want a long press gesture recognizer and not a pan gesture recognizer. Simply use that one and set some very small minimum press duration. Note that this gesture works exactly the same as the pan gesture but can have a small delay to be detected. It is very useful in these kind of situations.
Update (The architecture):
The hierarchy should be:
View controller -> Scrollview -> Canvas view -> Small views
The canvas view should contain the gesture recognizer that controls the small views. When the gesture begins you should check if any of the views are hit by its location by simply iterating through the subviews and check if their frame contains a point. If so it should start moving the hit small view and it should notify its delegate that it has began moving it. If not it should cancel the gesture recognizer.
As the canvas view has a custom delegate it is the view controller that should implement its protocol and assign itself to the canvas view as a delegate. When the canvas view reports that the view dragging has begin it should disable the scrollview scrolling. When the canvas view reports it has stopped moving the views it should reenable the scrolling of the scroll view.
Create this type of view hierarchy
Create a custom protocol of the canvas view which includes "did begin dragging" and "did end dragging"
When the view controller becomes active assign self as a delegate to the canvas view. Implement the 2 methods to enable or disable the scrolling of the scroll view.
The canvas view should add a gesture recognizer to itself and should contain an array of all the small movable subviews. The recognizer should be able to interact with other recognizers simultaneously which is done through its delegate.
The Canvas gesture recognizer target should on begin check if any of the small views are hit and save it as a property, it should also save the current position of the gesture. When the gesture changes it should move the grabbed view depending on the last and current gesture location and re-save the current location to the property. When the gesture ends it should clear the currently dragged view. On begin and end it should call the delegate to notify the change of the state.
Disable or enable the scrolling in the view controller depending on the canvas view reporting to delegate.
I think this should be all.
How can I add a Pan Gesture Recognizer to an UITableView, without blocking my scrolling feature of my TableView?
This is my code:
#IBOutlet var ScanPanGestureRecognizer: UIPanGestureRecognizer!
#IBAction func ScanPanGestureRecognizer(sender: UIPanGestureRecognizer)
{
print("TEST")
}
override func viewDidLoad()
{
ScanTableView.addGestureRecognizer(ScanPanGestureRecognizer)
}
So the code works and I get a lot of prints with "Test" but I'm not able to move (scroll) my TableView any more. I have read some other questions / answers but I couldn't find my issue. I thought that the extension "addGestureRecognizer" only add a gesture and not overwrite the TableView pan gesture... Thanks!
I think whatever you want to do with a pan gesture recognizer on a UITableView you can do it in UITableView's delegate method scrollViewDidScroll: in that method the scrollView.contentOffset will tell you how much the tableView has scrolled
I have a ios-chart as a subview that takes up half the screen. When I pan up on any other subview it scrolls. But not when I pan on the chart.
I tried setting:
[self.chart setDefaultTouchEventsEnabled:YES];
//and
[self.chart setScaleEnabled:NO];
It says in the documentation
defaultTouchEventsEnabled
enables/disables default touch events to be handled. When disable, touches are not passed to parent views so scrolling inside a UIScrollView won’t work.
What can I do to enable scrolling when panning/dragging on the chart?
I was struggling with this as well. Since I needed the chart to be scrollable, #Entrabiter's solution didn't work for me. The only solution that worked for me, was assigning the delegate of the chart view's UIPanGestureRecognizer to my ViewController and implement UIGestureRecognizerDelegate.
This solution is in Swift, but it should also work fine in Objective-C.
class MyViewController: UIViewController, UIGestureRecognizerDelegate {
// MARK: Outlets
#IBOutlet weak var contentView: UIScrollView!
#IBOutlet weak var myChart: LineChartView!
override func viewDidLoad() {
super.viewDidLoad()
if let gestureRecognizers = myChart.gestureRecognizers {
for gestureRecongnizer in gestureRecognizers {
if gestureRecongnizer is UIPanGestureRecognizer {
gestureRecongnizer.delegate = self
}
}
}
}
}
The important part is to tell the gestureRecognizer to recognize both the scrollview's panGestureRecognizer as well as the chartview's panGestureRecognizer.
// MARK: UIGestureRecognizerDelegate
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer == contentView.panGestureRecognizer {
return true
}
return false
}
Set userInteractionEnabled on the chart view to NO.
I think I figured it out...
I removed all the UIPanGestureRecognizers from the ios-charts subview.
This allowed for the scrollview to handle all the pan events.
for(UIGestureRecognizer *rec in self.chart.gestureRecognizers){
if([rec isKindOfClass:[UIPanGestureRecognizer class]]){
[self.chart removeGestureRecognizer:rec];
}
}