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).
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've had a lot of success in the past using MGSwipeTableCell to swipe to dismiss cells, but my current task calls to swipe an entire section in the same behavior.
I currently have a swipe gesture recognizer in the UITableView, when the swipe gesture is triggered, I calculate the section the touch was recieved, and delete the objects that populate that section (in core data), then call the delete animation:
//Delete objects that populate table datasource
for notification in notifications {
notificationObject.deleted = true
}
DataBaseManager.sharedInstance.save()
let array = indexPathsToDelete
let indexSet = NSMutableIndexSet()
array.forEach(indexSet.add)
//Delete section with animation
self.notificationsTableView.deleteSections(indexSet as IndexSet, with: .left)
This works, but is not ideal. Ideally we would like the whole section to drag with your finger (and when released at a certain point, it goes off screen), similar to MGSwipeTableCell. What is the best way to approach this? Is there another library which allows swipe to delete sections (I can't find any)? Or is this something I will have to create myself.
I haven't tested this but the idea is below. Take a view (self.header) and use the touchesBegan... method to detect the user placing their finger on screen. Then, follow the finger with the touchesMoved... method and calculate the difference between the last offset and the next. It should grow by 1 (or more) depending on how fast the user is moving their finger. Use this value to subtract the origin.x of the cell's contentView.
var header: UIView!
var tableView:UITableView!
var offset:CGFloat = 0
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Touches Began. Disable user activity on UITableView
if let touch = touches.first {
// Get the point where the touch started
let point = touch.location(in: self.header)
offset = point.x
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
// Get the point where the touch is moving in header
let point = touch.location(in: self.header)
// Calculate the movement of finger
let x:CGFloat = offset - point.x
if x > 0 {
// Move cells by offset
moveCellsBy(x: x)
}
// Set new offset
offset = point.x
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// Reset offset when user lifts finter
offset = 0
}
func moveCellsBy(x: CGFloat) {
// Move each visible cell with the offset
for cell in self.tableView.visibleCells {
// Place in animation block for smoothness
UIView.animate(withDuration: 0.05, animations: {
cell.contentView.frame = CGRect(x: cell.contentView.frame.origin.x - x, y: cell.contentView.frame.origin.y, width: cell.contentView.frame.size.width, height: cell.contentView.frame.size.height)
})
}
}
Brandon's answer is correct, however, INSPullToRefresh library has issues when using touches began and other touch delegate methods.
What I had to do was implement a UIPanGestureRecognizer and track the touch when that gesture recognizer event is fired
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:).
I am developing a keybaord extension for iOS. On iOS 9 the keys react imediatelly except for keys along left edge of the keyboard. Those react with around 0.2 second delay. The reason is that the touches are simply delivered with this delay to the UIView that is root view of my keyboard. On iOS 8 there is no such delay.
My guess is that this delay is cause by some logic that is supposed to recognize gesture for opening "running apps screen". That is fine but the delay on a keyboard is unacceptable. Is there any way how to get those events without delay? Perhaps just setting delaysTouchesBegan to false on some UIGestureRecognizer?
This is for anyone using later versions of iOS (this is working on iOS 9 and 10 for me). My issue was caused by the swipe to go back gesture interfering with my touchesBegan method by preventing it from firing on the very left edge of the screen until either the touch was ended, or the system recognised the movement to not be that of the swipe to go back gesture.
In your viewDidLoad function in your controller, simply put:
self.navigationController?.interactivePopGestureRecognizer?.delaysTouchesBegan = false
The official solution since iOS11 is overriding preferredScreenEdgesDeferringSystemGestures of your UIInputViewController.
https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys
However, it doesn't seem to work on iOS 13 at least. 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' not the case for UIInputViewController, though.
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:).
If you have access to the view's window property, you can access these system gesture recognizers and set delaysTouchesBegan to false.
Here's a sample code in swift that does that
if let window = view.window,
let recognizers = window.gestureRecognizers {
recognizers.forEach { r in
// add condition here to only affect recognizers that you need to
r.delaysTouchesBegan = false
}
}
Also relevant: UISystemGateGestureRecognizer and delayed taps near bottom of screen