Change shape of UICollectionViewCell gesture hitbox - ios

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.

Related

How to make the background view of a collectionView pass taps below

I'm building a collectionview. Below of it I placed some buttons as shown in the picture.
What I want is to make the UICollectionView background pass taps below, so the desired buttons can receive taps.
I don't need to add Tap gesture recognizers to the background view (the problem I'm describing is just an example here), I need the buttons' actuons to be triggered directly when they're tapped.
I thought I could do this by making the background clear or disabling user interaction for the background view. While disabling it for the entire collection view works, this other way does not.
How can I make the background view of my collectionView be "invisible" so that taps go straight to the below buttons instead of going to the collectionview background?
The following is an example of my layout.
Assuming your collectionView and your buttons share the same superview, this should do the trick.
What you want to do is bypass the backgroundView and forward hits to the subviews underneath the collectionView.
Notice that we are picking the last subview with the matching criteria. That is because the last subview in the array is the closest to the user's finger.
class SiblingAwareCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hit = super.hitTest(point, with: event)
guard hit === backgroundView else {
return hit
}
let sibling = superview?
.subviews
.filter { $0 !== self }
.filter { $0.canHit }
.last { $0.point(inside: convert(point, to: $0), with: event) }
return sibling ?? hit
}
}
If you look at the documentation for hitTest(_:with:) it says:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01.
For convenience, here is an extension to ensure we are playing by the rules:
extension UIView {
var canHit: Bool {
!isHidden && isUserInteractionEnabled && alpha >= 0.01
}
}

Detect When UITableViewCell is About To Leave The UITableView

I want to animate a UITableViewCell whenever it is about to leave the view. I know that there is a didEndDisplayingcell function, but by then, it is too late to animate the cell. Becuase it's gone. I basically want to cell to scale down using...
UIView.animate(withDuration: 0.2) {
self.cellView.transform = CGAffineTransform(translationX: self.view.frame.size.width, y: 0)
}
...and whenever it comes back on the screen, scale it back to normal.
Any ideas?
Not a complete solution, but this might get you there:
self.tableView.indexPathsForVisibleRows
Use this to find whats on the screen.
for path in self.tableView.indexPathsForVisibleRows! {
//check if it's past a point on the screen
//animate if needed
}
Check for these conditions in
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
// call the function to check if cells pass a threshold
}
The last bit will be setting it up so cells are fully visible on first load, but after that they start off in the state to be animated in.
The best way to approach this would be to check when the cell is finished being displayed via the UITableViewDelegate method:
func tableView(UITableView, didEndDisplaying: UITableViewCell, forRowAt: IndexPath)
Tells the delegate that the specified cell was removed from the table.

UICollectionView: Measure force on tap of cell

I am working on a tiny project where I want to use the 3D touch screen pressure capabilities (e.g. touch.force).
Right now I can measure force in my ViewController and it is behaving like I want it:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if traitCollection.forceTouchCapability == UIForceTouchCapability.available {
// 3D Touch capable
let force = touch.force
print("Force: " + force.description)
}
}
}
I have one UIImageView that presents a picture. I also use an custom UICollectionView as overlay, which contains transparent cells and colored cells. What I am trying to achieve is to measure both force, and be able to remove the color from the cells when they are pressed, but I am failing to do both. I can either measure force, or change the color of a cell in the UICollectionView, but not both.
The solution as I see it is to forward touches from the UICollectionView to the next view in the hierarchy. But another solution could be to measure both force and register taps on cells from within the custom UICollectionView.
My question is, is either of my proposed solutions any good? And if so, how do I achieve them?
Apparently the override func touches... methods are part of UIView, not neccesarily UIViewControllers. This means that implementing the touchesMoved and other methods into my custom UICollectionView is perfectly possible and it solved my problem of handling touches and taps at the same time.

Gesture recognizer on a circular view

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.

UIPanGesture inside UIScrollView in Swift

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.

Resources