Почему верхняя часть моего UISegmentedControl не поддерживается?

Пока я играл на своем телефоне, я заметил, что мой UISegmentedControl не очень отзывчив. Было бы две или более попыток сделать мой входной регистр. Поэтому я решил запустить свое приложение в Simulator, чтобы более точно определить, что было не так. Нажимая десятки раз с помощью мыши, я решил, что верхние 25% UISegmentedControl не отвечают (часть выделена красным цветом с помощью 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;

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

Ответы

Ответ 1

Как уже написано в других ответах, UINavigationBar захватывает штрихи, сделанные рядом с навигационной панелью, но не потому, что у нее есть некоторые подвидности, расширенные по краям: это не причина.

Если вы зарегистрируете всю иерархию представлений, вы увидите, что UINavigationBar не распространяется на определенные ребра.

Причина, по которой он получает штрихи, - это другое:

в UIKit существует много "особых случаев", и это один из них.

Когда вы нажимаете на экран, запускается процесс под названием "hit testing". Начиная с первого UIWindow, всем представлениям предлагается ответить на два "вопроса": точка, занятая вашими границами? каковы субподзоры, которые должны получать событие касания?

на эти вопросы отвечают эти два метода:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

Хорошо, теперь мы можем продолжить.

После крана, UIApplicationMain запускает процесс тестирования ударов. Тест атаки начинается с основного UIWindow (и выполняется, например, даже в окне строки состояния и в окне просмотра предупреждений) и проходит через все подпункты.

Этот процесс выполняется 3 раза:

  • два раза начиная с UIWindow
  • один раз, начиная с _UIApplicationHandleEvent

Если вы нажмете на панели навигации, вы увидите, что hitTest в UIWindow вернет UINavigationBar (все три раза)

Если вы нажмете на область под панелью навигации, вы увидите что-то странное:

  • первые два hitTest вернут ваш UISegmentedControl
  • последний hitTest вернет UINavigationBar

зачем это? Если вы swizzle и подкласс UIView, переопределяя hitTest, вы увидите, что первые два раза точка прослушивания верна. В третий раз что-то меняет точку, делая что-то вроде point - 15 (или аналогичного числа)

После многого поиска я нашел, где это происходит:

UIWindow имеет (частный) метод, называемый

-(CGPoint)warpPoint:(CGPoint)point;

отлаживая его, я увидел, что этот метод изменяет отмеченную точку, если она находится непосредственно под строкой состояния. Отлаживая больше, я увидел, что вызовы стека, которые делают это возможным, составляют всего 3:

[UINavigationBar, _isChargeEnabled]
[UINavigationBar, isEnabled]
[UINavigationBar, _isAlphaHittableAndHasAlphaHittableAncestors]

Итак, в конце этот метод warpPoint проверяет, включен ли UINavigationBar и hittable, если да, он "искажает" точку. Точка деформируется из числа пикселей от 0 до 15, и этот "деформация" увеличивается, когда вы приближаетесь к панели навигации.

Теперь, когда вы знаете, что происходит за кулисами, вы должны знать, как его избежать (если хотите).

Вы не можете просто переопределить warpPoint:, если приложение должно перейти в AppStore: это частный метод, и ваше приложение будет отклонено.

Вам нужно найти другую систему (например, предлагаемую, переопределяя sendEvent, но я не уверен, что она будет работать)

Поскольку этот вопрос интересен, я буду думать о юридическом решении завтра и обновить этот ответ (одной хорошей отправной точкой может быть подклассификация UINavigationBar, переопределяющая hitTest и pointInside, возвращающая nil/false, если, учитывая одно и то же событие по нескольким вызовам, но я должен проверить, работает ли это завтра)

ИЗМЕНИТЬ

Хорошо, я пробовал много решений, но не просто найти законный и стабильный. Я описал фактическое поведение системы, которое может варьироваться в разных версиях (hitTest называется более или менее 3 раз, warpPoint деформирует точку около 15 пикселей, что может изменить ecc ecc).

Наиболее стабильным, очевидно, является незаконное переопределение warpPoint: в подклассе UIWindow:

-(CGPoint)warpPoint:(CGPoint)point;
{
    return point;
}

однако, я обнаружил, что такой метод (в подклассе 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];
}

Этот метод записывает последнюю точку, и если последующее нажатие имеет один и тот же x, а y - для max 16px, то использует предыдущую точку. Я много тестировал, и он кажется стабильным. Если вы хотите, вы можете добавить дополнительные элементы управления, чтобы включить это поведение только в определенных контроллерах или только в определенной части окна, ecc ecc. Если я найду другое решение, я обновлю сообщение

Ответ 2

Я считаю, что проблема в том, что кнопки в UINavigationBar имеют большую, чем обычно, зону касания. См. Этот SO пост. Вы также можете найти много дискуссий по этому вопросу с помощью поиска в области поиска "UINavigationBar".

В качестве возможного решения вы можете поместить сегментированный элемент управления в навигационную панель, но вы бы знали лучше, чем я, если это подходит для ваших случаев использования или нет.

Ответ 3

Я придумал альтернативное решение, которое для меня кажется более безопасным, чем LombaX. Он использует тот факт, что оба события входят с одной и той же меткой времени, чтобы отклонить последующее событие.

@interface RFNavigationBar ()

@property (nonatomic, assign) NSTimeInterval lastOutOfBoundsEventTimestamp;

@end

@implementation RFNavigationBar

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // [rfillion 2014-03-28]
    // UIApplication/UIWindow/UINavigationBar conspire against us. There a band under the UINavigationBar for which the bar will return
    // subviews instead of nil (to make those tap targets larger, one would assume). We don't want that. To do this, it seems to end up
    // calling -hitTest twice. Once with a value out of bounds which is easy to check for. But then it calls it again with an altered point
    // value that is actually within bounds. The UIEvent it passes to both seem to be the same. However, we can't just compare UIEvent pointers
    // because it looks like these get reused and you end up rejecting valid touches if you just keep around the last bad touch UIEvent. So
    // instead we keep around the timestamp of the last bad event, and try to avoid processing any events whose timestamp isn't larger.
    if (point.y > self.bounds.size.height)
    {
        self.lastOutOfBoundsEventTimestamp = event.timestamp;
        return nil;
    }
    if (event.timestamp <= self.lastOutOfBoundsEventTimestamp + 0.001)
    {
        return nil;
    }
    return [super hitTest:point withEvent:event];
}

@end

Ответ 4

Возможно, вам захочется проверить, какое представление записывает штрихи. Попробуйте этот метод -

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    [touch locationInView:self.view];
    if([touch.view isKindOfClass:[UISegmentedControl class]])
    {
      NSLog(@"This is UISegment");
    }
    else if([touch.view isKindOfClass:[UITabBar class]]) 
    {
      NSLog(@"This is UITabBar");
    } else if(...other views...) {
        ...
    }
 }

Как только вы поймете это, вы, возможно, сможете сузить свою проблему.

Ответ 5

Похоже, вы используете расширение категории для установки ширины/высоты на представлениях, а также центрируете их в своем родителе. Возможно, здесь есть скрытая проблема - можете ли вы реорганизовать, чтобы сделать свой макет без этой категории?

Я скопировал ваш код в чистый проект и запустил его в UITableViewController viewDidLoad - он отлично работает, и у меня нет мертвых точек, как вы сообщаете. Мне пришлось немного изменить код, так как у меня нет того же расширения категории, которое вы используете.

Кроме того, если вы используете этот код в viewDidLoad, вы должны убедиться, что ваше представление имеет определенный размер (вы получаете доступ к вашему view.width). Если вы создаете свой UITableViewController программно (vs from nib/storyboard), тогда кадр может быть CGRectZero. Шахта была загружена из наконечника, чтобы кадр был установлен.

Я бы также попытался временно удалить ваш пограничный вид, чтобы увидеть, является ли он виновником.

Ответ 6

Я рекомендую вам избегать использования сенсорного интерфейса в такой непосредственной близости от навигационной панели или панели инструментов. Эти области обычно известны как "факторы отслоения", что облегчает пользователям возможность совершать сенсорные события на кнопках без сложностей с точными касаниями. Это также относится к UIButton, например.

Но если вы хотите захватить событие касания до того, как панель навигации или панель инструментов его получит, вы можете подклассифицировать UIWindow и переопределить: - (void) событие sendEvent: (UIEvent *);

Ответ 7

Простым способом отладки является попытка использовать DCIntrospect в вашем проекте. Это очень простая в использовании/реализующая библиотека, которая позволяет узнать, какие виды видны там, где в симуляторе ветерок.

  • Установите библиотеку и настройте ее
  • Запустите приложение в симуляторе и перейдите к экрану с проблемой
  • Нажмите клавишу пробела на клавиатуре (клавиатура компьютера, а не симулятор клавиатуры)
  • Нажмите на область 25% и посмотрите, что подсвечивается.

Если выделенное не является сегментированным контроллером представления, это представление может быть тем, что скрывает событие касания.

Ответ 8

Создайте протокол для UINavigationBar: (добавьте новый файл и вставьте ниже кода)

/******** file: UINavigationBar+BelowSpace.h*******/

"UINavigationBar+BelowSpace.h"

    #import <Foundation/Foundation.h>

@interface UINavigationBar (BelowSpace)

@end

/*******- file: UINavigationBar+BelowSpace.m*******/

#import "UINavigationBar+BelowSpace.h"

@implementation UINavigationBar (BelowSpace)


-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    int errorMargin = 5;// space left to decrease the click event area
    CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height);
    BOOL isTouchAllowed =  (CGRectContainsPoint(smallerFrame, point) == 1);

    if (isTouchAllowed) {
        self.userInteractionEnabled = YES;
    } else {
        self.userInteractionEnabled = NO;
    }
    return [super hitTest:point withEvent:event];
}
@end

Надеюсь на эту помощь ^ ^

Ответ 9

Попробуйте это

self.navigationController!.navigationBar.userInteractionEnabled = false;