In each cell of my collection view is a circular UIView. This has been achieved by creating a custom subclass of UIView, which I have called CircleView, and setting layer.cornerRadius = self.frame.size.width/2 in the subclass' awakeFromNib()
I want to add a gesture recognizer to each CircleView. I have done this in the collection view's cellForItemAtIndexPath:
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
cell.circleView.addGestureRecognizer(gestureRecognizer)
The problem is that the gesture recognizer is called whenever a tap occurs anywhere within the bounds of the original square UIView. I want to only recognize taps that occur within the circle.
I have tried to solve this issue in the following ways:
In the CircleView's awakeFromNib() I set self.clipsToBounds = true (no effect)
Also in the CircleView's awakeFromNib() I set layer.masksToBounds = true (no effect)
Thank you in advance for your ideas and suggestions.
You can override this method in CircleView:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let center = CGPoint(x: bounds.size.width/2, y: bounds.size.height/2)
return pow(center.x-point.x, 2) + pow(center.y - point.y, 2) <= pow(bounds.size.width/2, 2)
}
All touches not belonging to the circle will be ignored.
More details:
https://developer.apple.com/reference/uikit/uiview/1622469-hittest
https://developer.apple.com/reference/uikit/uiview/1622533-point
The main point is that you don't need to call neither hitTest nor pointInside methods, you just override them in your custom view and system will call them whenever it needs to know if a touch should be handled by this view.
In your case you've got a UITableViewCell with a CircleView in it, right? You've added a gesture recognizer to CircleView and overriden pointInside method, so a touch will be handled by CircleView itself if a touch point is inside the circle, otherwise event will be passed further, handled by cell and therefore didSelectRowAtIndexPath will be called.
Related
Let's say you have a superview that has a smaller size than its subview. You set the clipsToBound property of the superview to false. If you tap on the protruding area of the subview that is outside of the bounds of the superview, why does the hit test return nil?
My understanding is that the hit test starts from the subview and work its way up to the superview. So why does the property of the superview that is to be tested later than the subview matter? Or does the hit test start from the root to tree views like the view controller -> the main view -> subviews?
I found a custom hit-test from here, which does allow you to tap on the subview's area outside of the bounds of the superview, but I'm not sure why reversing the order of subviews make a difference(it works, I'm just not sure why). My example even only has one subview.
class ViewController: UIViewController {
let superview = CustomSuperview(frame: CGRect(origin: .zero, size: .init(width: 100, height: 100)))
let subview = UIView(frame: CGRect(origin: .zero, size: .init(width: 200, height: 200)))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.superview)
self.superview.backgroundColor = .red
self.superview.clipsToBounds = false
self.superview.addSubview(self.subview)
self.subview.backgroundColor = .blue
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
self.subview.addGestureRecognizer(tap)
}
#objc func tapped(_ sender: UIGestureRecognizer) {
print("tapped")
}
}
class CustomSuperview: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if clipsToBounds || isHidden || alpha == 0 {
return nil
}
for subview in subviews.reversed() {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
}
My understanding is that the hit test starts from the subview and work its way up to the superview.
Then your understanding is completely wrong. Let’s fix that. First, let's clear up some other misunderstandings:
The reversed is a total red herring; it has nothing to do with it. The subviews are always tested in reverse order, because a subview in front needs to take precedence over a subview behind.
The clipsToBounds is a total red herring too. All it does is change whether you can see a subview outside its superview; it does not have any effect on whether you can touch a subview outside its superview.
Okay, so how does this work? Let's take view V which contains a subview A. Let's suppose that A is outside V. Assume you can see A, and you tap A.
Now hit-testing begins. But here's the thing: it starts at the level of the window, and works its way down the view hierarchy. So the window starts by interrogating its subviews; there is just one, the main view of the view controller.
So now we recurse, and the main view of the view controller interrogates its subviews. One of those is V. "Hey, V, was the tap inside you?" "No!" (You have to agree that that is the correct answer, because we already said that A is outside V.)
So the main view of the view controller gives up on V, and never finds out that the tap was on A, because we never recursed down that far. So it reports back up the chain: "The tap was not on any of my subviews, so I have to report that the tap was on me." The tap has fallen through to the view controller's main view.
But you can change that behaviour by overriding the implementation of hitTest:
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
if let result = super.hitTest(point, with:e) {
return result
}
for sub in self.subviews.reversed() {
let pt = self.convert(point, to:sub)
if let result = sub.hitTest(pt, with:e) {
return result
}
}
return nil
}
I have a UIView that fill the entire screen, then I'm adding multiple small circle UIView within that container view, I want those small circle's UIView to be draggable using UIPanGestureRecognizer. But sometimes they happen to be on top of each other making the top UIView not clickable at all, it always select the bottom ones.
In the container UIView I implemented hitTest to be able to select only those child views.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for planet in self.myPlanets.values {
if let presentation = planet.layer.presentation(), presentation.frame.contains(point) {
return planet
}
}
return nil
}
How can I make the top view receive the click even instead of the bottom view?
I handled draggable views by creating a UIView subclass, adding a UIPanGestureRecognizer and updating based on its inputs.
Using this method, whichever view is on top will receive the touch and you don't have to override hitTest on the superview.
I also added a delegate to update constraints if the view is constrained to the superview. By setting the delegate the UIView or ViewController (whichever is delegate) can update the constraints for the views you want to move.
Here's a simple implementation:
// Delegate protocol for managing constraint updates if needed
protocol DraggableDelegate: class {
// tells the delegate to move
func moveByTranslation(_ change: CGPoint)
}
class DraggableView: UIView {
var dragDelegate: DraggableDelegate?
init() {
// frame is set later if needed by creator
super.init(frame: CGRect.zero)
configureGestureRecognizers()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// setup UIPanGestureRecognizer
internal func configureGestureRecognizers() {
let panGR = UIPanGestureRecognizer.init(target: self, action: #selector(didPan(_:)))
addGestureRecognizer(panGR)
}
#objc func didPan(_ panGR: UIPanGestureRecognizer) {
// get the translation
let translation = panGR.translation(in: self).applying(transform)
if let delegate = dragDelegate {
// tell delegate to move
delegate.moveByTranslation(translation)
} else {
// move self
self.center.x += translation.x
self.center.y += translation.y
}
// reset translation
panGR.setTranslation(CGPoint.zero, in: self)
}
}
Here's how I implemented the delegate callback in the view controller using this view, since my view used constraints:
/// Moves the tool tray when dragging is triggered via the pan gesture recognizer in the tool tray (an instance of DraggableView).
///
/// - Parameter change: The x/y change provided by the pan gesture recognizer.
func moveByTranslation(_ change: CGPoint) {
// only interested in the y axis movements for this example
// update the constraint that moves this view
let newPos = constraint_tooltray_yAxis.constant + change.y
// this function limited the movement of the view to keep it within bounds
updateToolTrayPosition(newPos)
}
I have a confusing situation. First, here's the code:
class MyView: SKView {
...
public override func awakeFromNib() {
super.awakeFromNib()
let gameScene = MyGameScene(...)
// ...
presentScene(gameScene)
let view = UIView()
view.frame = bounds
view.isUserInteractionEnabled = true
addSubview(view)
}
...
}
class MyGameScene: SKScene {
...
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touch received")
}
...
}
I have a SKView that's presenting a SKScene. After presenting it, I add a UIView that's filling the screen. However, when I start the app and tap on the screen, "touch received" is printed, which means the touch got through to the scene, which should be behind the view. Why is that?
And what can I do to prevent it?
I also tried adding a gesture recogniser to the view. It doesn't prevent the issue, though (even though I set cancelsTouchesInView to true)
Thanks for any help.
Seems you're trying to achieve some kind of a HUD. SceneKit is not pure UIKit so you'll need to adapt your approach - some ideas:
UIButton should still work as mentioned here:
added button to sceneKit view but it has a lag
Tap gesture recogniser attached to SCNView:
https://stackoverflow.com/a/42982177/5329717
If you really need this particular UIView you could render it as an image and use a SKSpriteNode with SKTexture and handle touches there.
Not really recommended, but as a last resort you could add your UIView directly to UIWindow
Your issue gets fixed if you add the view to the superview of the SKView (i.e add it to the presenting view controller).
So you can try doing something along these lines:
let view = UIView()
view.frame = bounds
view.isUserInteractionEnabled = true
superview?.addSubview(view)
Alternatively you can add it to the window, but this is not really recommended.
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.
I have a bunch of UICollectionViewCells which look circular (imagine a circular background image). I want to detect when I tap on one of them, but currently the gesture hitbox of the cells are square-shaped corresponding to the cell height and width. As a result, tapping on the corner of the square shape, which is a bit outside of the circular look, still registers as a gesture:
I'm thinking of a way to do this while still being able to use the indexPathForItemAtPoint(point:) method i.e. I don't want to write a manual check, such as a for loop running cell.frame.containsPoint(point:).
I tried to change the borderRadius of the cell's layer, but I think the relevant field used to detect the tap is the frame, which is a CGRect...so is it even possible to achieve what I'm trying to do, or must I write the cell detection code myself? (something like, get all cells and calculate the distance to the center, and then see if the distance is within the radius)
The main problem is that my layout causes cell frames to overlap, so indexPathForItemAtPoint(point:) currently causes some cells to be favored over others when their frames overlap (most likely based on iteration order through visibleCells).
You can create a UIBezierPath and check if the tap is within that. For example in your action for a UITapGestureRecognizer you can say something like
let location = sender.location
let indexPath = collectionView.indexPathForItemAtPoint(location)
let cell = collectionView.cellForItemAtIndexPath(indexPath!)
let newLocation = cell?.convertPoint(location, fromView: collectionView)
let circlePath = UIBezierPath(ovalInRect: (cell?.bounds)!)
if circlePath.containsPoint(newLocation!)
{
//handle cell tap
}
Or in your subclass of UICollectionViewCell you can add something like:
override func hitTest(point: CGPoint, withEvent e: UIEvent?) -> UIView? {
let circlePath = UIBezierPath(ovalInRect: (self.bounds))
if circlePath.containsPoint(point)
{
return self
}
return nil
}
This will check if the tap is in the visible part of the view or pass the event to lower views. Then implement didSelectItemAtIndexPath and handle the tap event there.