为什么我的UISegmentedControl的顶部不可点击?

Pwn*_*ner 15 cocoa-touch uisegmentedcontrol ios

当我在手机上玩时,我注意到我的UISegmentedControl响应不是很快.需要2次或更多次尝试才能使我的水龙头注册.所以我决定在Simulator中运行我的应用程序,以更准确地探测出错了什么.通过用鼠标点击几十次,我确定UISegmentedControl的前25%没有响应(下面的屏幕截图中的部分用Photoshop突出显示为红色).我不知道有什么看不见的UIView可以阻止它.你知道怎么让整个控件可以安装吗?

uinavigationbar uisegmentedcontrol

self.segmentedControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Uno", @"Dos", nil]];
self.segmentedControl.selectedSegmentIndex = 0;
[self.segmentedControl addTarget:self action:@selector(segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
self.segmentedControl.height = 32.0;
self.segmentedControl.width = 310.0;
self.segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
self.segmentedControl.tintColor = [UIColor colorWithWhite:0.9 alpha:1.0];
self.segmentedControl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;

UIView* toolbar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, HEADER_HEIGHT)];
toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
CAGradientLayer *gradient = [CAGradientLayer layer];
    gradient.frame = CGRectMake(
        toolbar.bounds.origin.x,
        toolbar.bounds.origin.y,
        // * 2 for enough slack when iPad rotates
        toolbar.bounds.size.width * 2,
        toolbar.bounds.size.height
    );
    gradient.colors = [NSArray arrayWithObjects:
        (id)[[UIColor whiteColor] CGColor],
        (id)[[UIColor 
            colorWithWhite:0.8
            alpha:1.0
            ] CGColor
        ],
        nil
];
[toolbar.layer insertSublayer:gradient atIndex:0];
toolbar.backgroundColor = [UIColor navigationBarShadowColor];
[toolbar addSubview:self.segmentedControl];

UIView* border = [[UIView alloc] initWithFrame:CGRectMake(0, HEADER_HEIGHT - 1, toolbar.width, 1)];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
border.backgroundColor = [UIColor colorWithWhite:0.7 alpha:1.0];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[toolbar addSubview:border];

[self.segmentedControl centerInParent];

self.tableView.tableHeaderView = toolbar;
Run Code Online (Sandbox Code Playgroud)

http://scs.veetle.com/soget/session-thumbnails/5363e222d2e10/86a8dd984fcaddee339dd881544ecac7/5363e222d2e10_86a8dd984fcaddee339dd881544ecac7_20140509171623_536d6fd78f503_68_896x672.jpg

Lom*_*baX 16

正如其他答案中已经写过的那样,UINavigationBar抓住了导航栏本身附近的触摸,但并不是因为它有一些扩展到边缘的子视图:这不是原因.

如果记录整个视图层次结构,您将看到UINavigationBar不会在定义的边上延伸.

接收触摸的原因是另一个:

在UIKit中,有许多"特殊情况",这就是其中之一.

点击屏幕时,会启动一个名为"命中测试"的过程.从第一个UIWindow开始,要求所有视图回答两个"问题":点在你的界限内?什么是必须接收触摸事件的子视图?

这两个方法回答了这个问题:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
Run Code Online (Sandbox Code Playgroud)

好的,现在我们可以继续了.

点击后,UIApplicationMain启动命中测试过程.命中测试从主UIWindow开始(例如,甚至在状态栏窗口和警报视图窗口中执行),并遍历所有子视图.

此过程执行3次:

  • 两次从UIWindow开始
  • 一次从_UIApplicationHandleEvent开始

如果点击导航栏,您将看到hitTestUIWindow将返回UINavigationBar(全部三次)

如果你点击导航栏下面的区域,你会发现一些奇怪的东西:

  • 前两个hitTest将返回你的UISegmentedControl
  • 最后一个hitTest将返回UINavigationBar

为什么这个?如果你在UIView上进行调配和子类化,重写hitTest,你会发现前两次点击点是正确的.第三次,某些东西改变了做某事point - 15(或类似数字)的观点

经过大量的搜索,我发现了这种情况:

UIWindow有一个名为的(私有)方法

-(CGPoint)warpPoint:(CGPoint)point;
Run Code Online (Sandbox Code Playgroud)

调试它,我看到这个方法改变了轻敲点,如果它位于状态栏的正下方.调试更多,我看到堆栈调用使这成为可能,只有3:

[UINavigationBar, _isChargeEnabled]
[UINavigationBar, isEnabled]
[UINavigationBar, _isAlphaHittableAndHasAlphaHittableAncestors]
Run Code Online (Sandbox Code Playgroud)

因此,最后,此warpPoint方法检查UINavigationBar是否已启用且是可命中的,如果是,则"扭曲"该点.该点在0到15之间扭曲了许多像素,当您靠近导航栏时,这种"扭曲"会增加.

既然您知道幕后发生了什么,您必须知道如何避免它(如果您愿意).

warpPoint:如果应用程序必须在AppStore上运行,您不能简单地覆盖:它是一个私有方法,您的应用程序将被拒绝.

你必须找到另一个系统(如建议,覆盖sendEvent,但我不确定它是否会起作用)

因为这个问题很有意思,我明天会考虑一个合法的解决方案并更新这个答案(一个好的起点可以是UINavigationBar的子类,重写hitTest和pointInside,如果在多个调用中给出相同的事件,则返回nil/false,点变化但我必须测试明天是否有效)

编辑

好吧,我尝试了很多解决方案,但找到合法且稳定的解决方案并不简单.我已经描述了系统的实际行为,在不同的版本上可能会有所不同(hitTest调用多于或少于3次,warpPoint扭曲大约15px的点可以改变ecc ecc).

最稳定的显然warpPoint:是UIWindow子类中的非法覆盖:

-(CGPoint)warpPoint:(CGPoint)point;
{
    return point;
}
Run Code Online (Sandbox Code Playgroud)

但是,我发现像这样的方法(在UIWindow子类中)它足够稳定并且可以解决问题:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // this method is not safe if you tap the screen two times at the same x position and y position different for 16px, because it moves the point
    if (self.lastPoint.x == point.x)
    {
        // the points are on the same vertical line
        if ((0 < (self.lastPoint.y - point.y)) && ((self.lastPoint.y - point.y) < 16) )
        {
            // there is a differenc of ~15px in the y position?
            // if so, the point has been changed
            point.y = self.lastPoint.y;
        }
    }

    self.lastPoint = point;

    return [super hitTest:point withEvent:event];
}
Run Code Online (Sandbox Code Playgroud)

此方法记录最后一个点击的点,如果后续点击位于同一个x,并且y最大值为16px,则使用前一个点.我测试了很多,看起来很稳定.如果需要,可以添加更多控件以仅在特定控制器中启用此行为,或仅在窗口的已定义部分(ecc ecc)上启用此行为.如果我找到另一个解决方案,我会更新帖子

  • 这是非法的,因为你正在调整私人方法.Apple似乎使用静态分析器在可执行文件中查找私有符号,因此如果您尝试在运行时调用从字符串构造选择器的方法(例如:@"war"+ @"P"+ @"oint") ,Apple可能很难找到你使用私有方法(但它仍然是非法的).找到一个合法的解决方案并不简单,现在我发布了更简单的发现 (2认同)