I have a table view cell that has a button on the top of the view so that part of it not in the cell. So i have done this in side the custom tableview cell class. -
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
if CGRectContainsPoint(buttonA.frame, point) {
buttonAPressed()
return true
} else if CGRectContainsPoint(buttonB.frame, point) {
buttonBPressed()
return true
} else if CGRectContainsPoint(buttonC.frame, point) {
buttonCPressed()
return true
}
return super.pointInside(point, withEvent: event)
}
So that the button hits are detected for the buttons. The problem is that I also have a big button behind the tableview. The problem is that the button behind is also detected as being pressed. Is there a way to stop the behind button from being pressed?
Related
I am creating an app so that user has to Swipe all the boxes from the screen. The goal is to swipe all the boxes until all boxes are swiped like example below.
So my question is:
Is it better to create the boxes using Stack View or rather draw manually by coordinates on the screen?
How to detect if user has swiped through the boxes (using UIGestureRecognizer)?
Note: When user swiped through the boxes, swiped boxes will turn into other color.
Both stack view or manually should work nicely. I would go with manually in this case but this is just a preference because you might have more power over it. But there is a downside that you need to reposition them when screen size changes. A third option is also a collection view.
The gesture recognizer should be pretty straight forward. You just add it on the superview of these cells and check the location when it moves or and when it starts. A pan gesture seems the most appropriate but it will not detect if user just taps the screen. This may be a feature but if you want to handle all touches you should either use a long press gesture with zero press duration (It makes little sense, I know but it works), or you may simply just override touch methods:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
handleDrag(at: touch.location(in: viewWhereAllMiniViewsAre))
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
handleDrag(at: touch.location(in: viewWhereAllMiniViewsAre))
}
}
func handleDrag(at location: CGPoint) {
// TODO: handle the nodes
}
The gesture recognizer procedure would do something like:
func onDrag(_ sender: UIGestureRecognizer) {
switch sender.state {
case .began, .changed, .ended, .cancelled: handleDrag(at: sender.location(in: viewWhereAllMiniViewsAre))
case .possible, .failed: break
}
}
Now all you need is your data source. An array of all of your items should be enough. Like:
static let rows: Int = 10
static let columns: Int = 10
var nodes: [Node] = {
return Array<Node>(repeating: Node(), count: LoginViewController.rows * LoginViewController.columns)
}()
And a list of all of your mini views:
var nodeViews: [UIView] = { ... position them or get them from stack view or from collection view }
Now the implementation on touch handle:
func handleDrag(at location: CGPoint) {
nodeViews.enumerated().forEach { index, view in
if view.frame.contains(location) {
view.backgroundColor = UIColor.green
nodes[index].selected = true
}
}
}
This is just an example. An easy one and rather a bad one from maintenance perspective at least. In general I would rather have a node view of custom UIView subclass with a reference to a node. Also it should hook using delegate to a Node instance so that the node reports when the selection state changes.
This way you have much cleaner solution when handling touches:
func handleDrag(at location: CGPoint) {
nodeViews.first(where: { $0.frame.contains(location) }).node.selected = true
}
Checking if all are green is then just
var allGreen: Bool {
return !nodes.contains(where: { $0.selected == false })
}
I have a parent view which contains two elements. Essentially, they comprise a dropdown select. Referencing the below image: When the blue element is clicked, it shows an (initially hidden) dropdown UITableView. This UITableView is partially inside of the same parent view that also contains the blue element.
When I try to click on one of the UITableViewCells, only the first cell registers a touch event. If the table view is situated such that the first cell is partly inside of the parent, only clicks on the half of the image that is inside of the parent register.
This seems to be a hierarchy issue. I have tried:
Adjusting Z-indices
Situating the entire UITableView outside of the parent in the Storyboard hierarchy, but visually positioning it inside of it.
I'm not sure how to proceed.
EDIT:
See Andrea's answer which worked for me. I ended up overriding the point method as suggested, and did not use hitTest. However, I went with another implementation of the point method override:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if super.point(inside: point, with: event) { return true }
for subview in subviews {
let subviewPoint = subview.convert(point, from: self)
if subview.point(inside: subviewPoint, with: event) { return true }
}
return false
}
As DonMag wrote:
You cannot interact with an element that extends beyond the bounds of
its parent (superview).
At least not without overriding some methods, views have their own implementations to detect touches. Most important methods are:
func hitTest(_ point: CGPoint,
with event: UIEvent?) -> UIView?
func point(inside point: CGPoint,
with event: UIEvent?) -> Bool
Those methods must be overriden, I've pasted some code on mine, is pretty old thus probably will require swift migration from 3.x to 4.x. Everything that you will add as a subview will handle touches even if is outside the bounds:
class GreedyTouchView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.clipsToBounds && !self.isHidden && self.alpha > 0.0 {
let subviews = self.subviews.reversed()
for member in subviews {
let subPoint = member.convert(point, from: self)
if let result: UIView = member.hitTest(subPoint, with:event) {
return result
}
}
}
return super.hitTest(point, with: event)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return super.point(inside: point, with: event)
}
}
But I'd like to point out that drop down menus are more Web UI related and not iOS UI related, you should use a picker instead.
I have a view in my storyboard that by default the alpha is set to 0. In certain cases the Swift file sets the alpha to 1. So either hidden or not. Before this view just contained 2 labels. I'm trying to add 2 buttons to the view.
For some reason the buttons aren't clickable at all. So when you tap it normally buttons change color slightly before you releasing, or while holding down on the button. But that behavior doesn't happen for some reason and the function connected to the button isn't being called at all.
It seems like an issue where something is overlapping or on top of the button. The button is totally visible and enabled and everything but not clickable. I tried Debug View Hierarchy but everything looks correct in that view.
Any ideas why this might be happening?
EDIT I tried making a class with the following code and in interface builder setting the container view to be that class.
class AnotherView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if view.isUserInteractionEnabled, view.point(inside: self.convert(point, to: view), with: event) {
return true
}
}
return false
}
}
Go with hitTest(_:with:) method. When we call super.hitTest(point, with: event), the super call returns nil, because user interaction is disabled. So, instead, we check if the touchPoint is on the UIButton, if it is, then we can return UIButton object. This sends message to the selector of the UIButton object.
class AnotherView: UIView {
#IBOutlet weak var button:UIButton!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if self.button.frame.contains(point) {
return button
}
return view
}
#IBAction func buttnTapped(sender:UIButton) {
}
}
I have a Map View (Google Maps not MapKit) underneath a UICollectionsView and would like to allow the map to be panned/zoomed but since the UICollectionView is set to take up the whole screen, it intercepts all touch events leaving the map static.
For some reference, this is what I currently have and the reason I'm setting the UICollectionView to be fullscreen is so I can animate the positions of the cells individually within all the screen space.
I've tried rewriting this answer in Swift but it's throwing a nil when unwrapping :
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let indexPath: IndexPath? = self.indexPathForItem(at: point)
let cell: UICollectionViewCell? = (self.cellForItem(at: indexPath!))
if (cell != nil) && convert(point, to: cell?.contentView).x >= 0 {
return true
}
return false
}
Figured it out. Rather than detecting whether the touch event happened inside a cell, I just checked if the event happened past a certain y point and handled it accordingly.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if point.y > cardContainerY {
return true
}
return false
}
Where cardContainerY is a CGFloat that's calculated off of the currently focused card (since the cards can be expanded, moved within the y space).
I'm creating a Custom Keyboard for iOS. I have 4 rows of keys, each key have two actions: Touch Down to highlight button, and Touch Up Inside to unhighlight the button in 0.4 seconds.
But at the left edge of the screen there is a zone where Touch Down event of any button makes a delay for about quarter of second to show highlight.
See the image
So to see highlighted version, I had to hold the button, or swipe right from it. The buttons are the same, no difference at all. When I switch from letters to symbols, this left edge zone also makes the same delay. I've tried to move all the keys to the right, about 20px, and they worked fine, without delay. Ok, stick to the edge back, and delay came back also. Then I noticed, that pressing the button on its right edge, about 1-2 pixels made no delay at all. So, it seems like the problem is in this left side edge zone of the screen particularly.
By the way, I am running this app on my 5S, I've tried to run it on my friend's 5C, the same problem. But when I run it in the simulator, there is no such delay.
Use new iOS 11 feature to solve this problem definitely.
var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { get }
Documentation
I'm too creating a custom keyboard, and as far as I understand, that happens due to preferredScreenEdgesDeferringSystemGestures not working properly when overridden inside UIInputViewController, at least on iOS 13.
When you override this property in a regular view controller, it works as expected:
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return [.left, .bottom, .right]
}
That's however not the case for UIInputViewController.
UPD: It appears, gesture recognizers will still get .began state update, without the delay. So, instead of following the rather messy solution below, you can add a custom gesture recognizer to handle touch events.
You can quickly test this adding UILongPressGestureRecognizer with minimumPressDuration = 0 to your control view.
Another solution:
My original workaround was calling touch down effects inside hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?, which is called even when the touches are delayed for the view.
You have to ignore the "real" touch down event, when it fires about 0.4s later or simultaneously with touch up inside event. Also, it's probably better to apply this hack only in case the tested point is inside ~20pt lateral margins.
So for example, for a view with equal to screen width, the implementation may look like:
let edgeProtectedZoneWidth: CGFloat = 20
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
guard result == self else {
return result
}
if point.x < edgeProtectedZoneWidth || point.x > bounds.width-edgeProtectedZoneWidth
{
if !alreadyTriggeredFocus {
isHighlighted = true
}
triggerFocus()
}
return result
}
private var alreadyTriggeredFocus: Bool = false
#objc override func triggerFocus() {
guard !alreadyTriggeredFocus else { return }
super.triggerFocus()
alreadyTriggeredFocus = true
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
alreadyTriggeredFocus = false
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
alreadyTriggeredFocus = false
}
...where triggerFocus() is the method you call on touch down event. Alternatively, you may override touchesBegan(_:with:).