I have a floating UIView header below a UINavigationBar. I have inserted it as a subview at index 0, so I can animate it away and in according to contentOffset of a UITableView.
However, because it's beyond the bounds of the UINavigationBar I cannot receive touch events in this view after adding a UITapGestureRecognizer to it. All touch events go to the UITableView below it.
Any ideas if it's possible to have a subview outside of the bounds of the navigation bar and receive touch events for it?
I did this because adding it as a subview of a UITableView, I'd have to set the Y origin based on the contentOffset while scrolling and this also makes animations very difficult since the Y origin changes during scroll, so I can't know where to animate it to.
It's similar to the header in the Facebook app with "Status", "Photo" and "Check In" buttons.
Thanks
You need to create custom UINavigationBar and reimplement pointInside:withEvent: to extend touchable area of navigation bar (add your view area):
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect extendedRect = self.frame;
extendedRect.size.height += MENU_HEADER_HEIGHT - self.frame.origin.y;
BOOL pointInside = NO;
if (CGRectContainsPoint(extendedRect, point)) {
pointInside = YES;
}
return pointInside;
}
Adding to the above answer. Here is what I did.
class CustomNavigationBar: UINavigationBar {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if(self.pointInside(point, withEvent: event)){
self.userInteractionEnabled = true
}else{
self.userInteractionEnabled = false
}
return super.hitTest(point, withEvent: event)
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
var extendedRect:CGRect = self.frame
extendedRect.size.height += 100.0 - self.frame.origin.y
var pointInside:Bool = false
if(CGRectContainsPoint(extendedRect, point)){
pointInside = true
}
return pointInside
}
}
As far as I know you can't receive touch events outside the bounds of a view. So, I see a couple options:
First, you could go with the option of adding it as a subview to the UITableView, as you said, then adjusting its Y origin based on the tableview's contentOffset. When you want to animate it, instead of animating it to a specific y position you could do something like this:
[UIView animateWithDuration:0.4
animations:^{
_myHeaderView.frame = CGRectOffset(_myHeaderView.frame, 0.0, -_myHeaderView.frame.size.height);
}];
Another option I could see is adding the header view on top of everything, but with its origin sitting just below the UINavigationBar. Then when you animate it you could just move it to be below the UINavigationBar (or even actually pass it to the UINavigationBar) and animate after that so that it slides up underneath.
Swift 4 solution:
class TappableNavigationBar: UINavigationBar {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
isUserInteractionEnabled = self.point(inside: point, with: event)
return super.hitTest(point, with: event)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var extendedRect = frame
extendedRect.size.height += 300.0 - frame.origin.y
return extendedRect.contains(point)
}
}
Related
I created fake space between UITableViewCell by insetting the content.
contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0))
However, the UITableCell still accepts selection events based when selecting between the cells because even the non-inset 8 pixels is still contained within the UITableViewCell.
Is there a proper way of doing this so that selection does not occur?
I feel like this is a hack, so I am wondering what the more normal way of doing things is. I kind of would expect a property on the tableview itself to set the inset from interface builder
You can always override hitTest or pointInside in your tableView cell. You have mentioned that you are setting the contentView.frame assuming that its updating fine, you can simply use one of the below implementation
class SOTableViewCell: UITableViewCell {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if contentView.frame.contains(point) {
return super.hitTest(point, with: event) /*return self*/
}
return nil
}
}
Or
class SOTableViewCell: UITableViewCell {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return contentView.frame.contains(point)
}
}
As the following image,I have a collectionView, with a maskView above it.
And a right side view above the maskView.
How to implement the mask view event penetrate?
My PM's idea is that the collectionViewCell is clickable when a mask view above the collectionView. ( a little weird )
The mask view has taken over the collectionView's event.
I think I should override the event responder chain.
I tried to handle the override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
class Mask: UIView {
// ...
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let views = superview?.subviews else {
return self
}
for view in views{
if type(of: view) == UICollectionView.self{
return view
}
}
return self
}
}
Not worked as commanded, the collectionView just scrolls.
How to solve it?
I improved the code a little.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let views = superview?.subviews else {
return self
}
for view in views{
if type(of: view) == UICollectionView.self{
let collectionView = view as! UICollectionView
for cell in collectionView.visibleCells{
if cell.frame.contains(point){
return cell
}
}
}
}
return self
}
It is not very sharp. Some events are abandoned when switching the cells being clicked. Just like Apple did a hitTest event cache.
If I understood correctly and the mask view is on top of the collectionView and it gets the events you can just set the mask view userInteractionEnabled to false.
This way the event will be sent to the layer below until someone handles it.
I have a strong feeling that this might be a bug with Xcode that apple just needs to fix. I have a view controller that is embedded in a navigation controller as the root controller. The view controller has a button that is vertically aligned with the top layout guide. The view controller's button works fine and all of the tappable area is working.
However, if I push the same or a similar view controller on to the navigation stack, it's button will not be completely tappable. The top of the button (about 10 pixels or so) is no longer tappable. If I try to tap at the top left of the button, the back button on the navigation bar is tapped instead, even though I am clearly not tapping the navigation bar bounds. I assume this is a bug with apple but I was wondering if anyone knows of a fix. Here is the link to the github project if anyone needs it.
This is the default behavior for iOS. Many UIViews have this extended touch functionality in iOS. For example, UINavigationBar, UITabBar, UISecgmentedControl etc. I believe this is to make easer touch to these controls.
If you still want to override this default behavior. You can do this by subclassing UINavigationBar and add this method in the subclass:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
A merge request to your Github project is made.
Here's a swift version of the solution in case anyone needs it:
class CustomNavBar: UINavigationBar {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if pointInside(point, withEvent: event) {
userInteractionEnabled = true
} else {
userInteractionEnabled = false
}
return super.hitTest(point, withEvent: event)
}
}
Swift 3 and higher:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
isUserInteractionEnabled = self.point(inside: point, with: event)
return super.hitTest(point, with: event)
}
I am trying to build similar controller to GMSPlacePicker.
I have Map View on background, then Table View with transparent header view. The problem is that all gestures (tap, pan) within header view are passed to table view. I wanna disable them, so all touches will go directly to map view.
I am able to do it If I set:
tableView.userInteractionEnabled = false
but now I am not able to scroll table view.
The question is how to disable all gesture only for header view, but keep getting them for table view.
Basically I wanna get the following behaviour: https://youtu.be/iSBbEZXDyGg
The trick was to create subclass of UITableView and override
func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
ANTableView.swift
import UIKit
class ANTableView: UITableView
{
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
{
let headerViewFrame = tableHeaderView!.convertRect(tableHeaderView!.frame, toView: self)
if CGRectContainsPoint(headerViewFrame, point) {
return nil
}
return self
}
}
You need to put your map inside the header view. Not behind it.
I am trying to create a very simplified layout, something like Stripe's iOS app has which you can see here: https://stripe.com/img/dashboard_iphone/screencast.mp4
At around 0:06s the table view is swiped up and it moves up to the top of the window.
Are there any simple instructions in Swift that show this design pattern I see it everywhere but no idea how to create it
Add a UIView
Subclass that UIView with a custom class
In you custom UIView, you'll need a couple of variables and a few overrides. Make sure that user interaction is enabled on the UIView in your storyboard, then in your custom class add:
var startPosition: CGPoint?
var originalHeight: CGFloat?
After that, add the following:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
startPosition = touch?.locationInView(self)
originalHeight = self.frame.height
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let endPosition = touch?.locationInView(self)
let difference = endPosition!.y - startPosition!.y
let newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y + difference, self.frame.width, self.frame.height - difference)
self.frame = newFrame
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
In UIScrollView.h:
// When the user taps the status bar, the scroll view beneath the touch which is closest to the status bar will be scrolled to top, but only if its `scrollsToTop` property is YES, its delegate does not return NO from `shouldScrollViewScrollToTop`, and it is not already at the top.
// On iPhone, we execute this gesture only if there's one on-screen scroll view with `scrollsToTop` == YES. If more than one is found, none will be scrolled.
var scrollsToTop: Bool // default is YES.
Initialize your scroll view delegate (unless you already have, probably in your viewDidLoad), and then set this to true.
Like this:
myScrollView.scrollsToTop = true
If you want to be sure your scroll view delegate allows this, check this method from UIScrollViewDelegate's protocol:
optional func scrollViewShouldScrollToTop(scrollView: UIScrollView) -> Bool // return a yes if you want to scroll to the top. if not defined, assumes YES
Please comment any concerns.