Выравнивание элементов кнопки панели UINavigationBar в iOS 11

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

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

Редактировать:
Я повторно протестировал iOS 11 beta 2, и проблема осталась.

Редактировать 2: я повторно протестировал iOS beta 3, и проблема осталась.


person Georgi Boyadzhiev    schedule 21.06.2017    source источник


Ответы (10)


Об этом есть хорошая статья: http://www.matrixprojects.net/p/uibarbuttonitem-ios11/

Используя это, мы можем, по крайней мере, сдвигать элементы правой панели вправо до тех пор, пока они не оставят отступ в 8 пикселей от хвоста UINavigationBar.

Очень хорошо объяснил.

person Mayur Kothawade    schedule 29.11.2017
comment
работает как очаровательный после взлома по ссылке ... !! - person Sagar Daundkar; 29.11.2017
comment
Пометка как принятый ответ, потому что он ближе всего к решению, которое я искал во время вопроса. Ваше здоровье! - person Georgi Boyadzhiev; 29.11.2017

Теперь в iOS 11 вы можете управлять UINavigationBarContentView для настройки левого и правого ограничений и UIStackView для настройки кнопок (или других элементов).

Это мое решение для панели навигации с элементами слева и справа. Также это исправляет, если у вас есть несколько кнопок вместе на одной стороне.

- (void) updateNavigationBar {
    for(UIView *view in self.navigationBar.subviews) {
        if ([NSStringFromClass([view class]) containsString:@"ContentView"]) {

            // Adjust left and right constraints of the content view 
            for(NSLayoutConstraint *ctr in view.constraints) {
                if(ctr.firstAttribute == NSLayoutAttributeLeading || ctr.secondAttribute == NSLayoutAttributeLeading) {
                    ctr.constant = 0.f;
                } else if(ctr.firstAttribute == NSLayoutAttributeTrailing || ctr.secondAttribute == NSLayoutAttributeTrailing) {
                    ctr.constant = 0.f;
                }
            }

            // Adjust constraints between items in stack view
            for(UIView *subview in view.subviews) {
                if([subview isKindOfClass:[UIStackView class]]) {
                    for(NSLayoutConstraint *ctr in subview.constraints) {
                        if(ctr.firstAttribute == NSLayoutAttributeWidth || ctr.secondAttribute == NSLayoutAttributeWidth) {
                            ctr.constant = 0.f;
                        }
                    }
                }
            }
        }
    }
}


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

person JonyMateos    schedule 03.10.2017

Примерно через два дня, вот самое простое и безопасное решение, которое я смог придумать. Это решение работает только с пользовательскими кнопками панели просмотра, код для которых включен. Важно отметить, что левое и правое поля на панели навигации не изменились с iOS10 на iOS11 — они по-прежнему равны 16px. Такой большой запас затрудняет создание достаточно большой области щелчка.

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

Подкласс UINavigationBar

FWNavigationBar.h

@interface FWNavigationBar : UINavigationBar

@end

FWNavigationBar.m

#import "FWNavigationBar.h"

@implementation FWNavigationBar

- (void)layoutSubviews {
    [super layoutSubviews];

    if (@available(iOS 11, *)) {
        self.layoutMargins = UIEdgeInsetsZero;
        
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass([subview class]) containsString:@"ContentView"]) {
                subview.layoutMargins = UIEdgeInsetsZero;
            }
        }
    }
}

@end

Использование UINavigationController

#import "FWNavigationBar.h"

UINavigationController *controller = [UINavigationController initWithNavigationBarClass:[FWNavigationBar class] toolbarClass:nil];
[controller setViewControllers:@[rootViewController] animated:NO];

Создание кнопки UIBarButton

Поместите этот код либо в категорию UIBarButton, либо в файл, который вы планируете использовать с помощью кнопки панели, который вернет идентичный элемент кнопки панели с использованием UIButton.

+ (UIBarButtonItem *)barButtonWithImage:(UIImage *)image target:(id)target action:(SEL)action {
   UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
   //Note: Only iOS 11 and up supports constraints on custom bar button views
   button.frame = CGRectMake(0, 0, image.size.width, image.size.height);

   button.tintColor = [UIColor lightGrayColor];//Adjust the highlight color

   [button setImage:image forState:UIControlStateNormal];
   //Tint color only applies to this image
   [button setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateHighlighted];
   [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    
   return [[UIBarButtonItem alloc] initWithCustomView:button];
}

Настройка кнопки панели в вашем контроллере

- (void)viewDidLoad {
    [super viewDidLoad];

    self.navigationItem.leftBarButtonItem = [UIBarButtonItem barButtonWithImage:[UIImage imageNamed:@"your_button_image"] target:self action:@selector(leftButtonPressed)];
}

Наконец, я бы рекомендовал оставить левое и правое поля равными нулю и просто отрегулировать положение кнопки на вашем изображении. Это позволит вам использовать всю интерактивную область до края экрана. То же самое касается высоты вашего изображения — убедитесь, что высота составляет 44 пункта.

person Josh Bernfeld    schedule 10.10.2017
comment
Представление содержимого панели навигации — это частный класс. Есть ли риск отказа магазина приложений, если он будет изменен таким образом? - person Georgi Boyadzhiev; 10.10.2017
comment
Вышеупомянутое выполняется во время выполнения, и в двоичный файл не добавляются частные строки/селекторы, поэтому этого не должно быть. Риск есть, но невелик. - person Josh Bernfeld; 10.10.2017
comment
лучшее решение, которое у меня когда-либо было, сделай мой день. - person Carusd Ray; 22.12.2017

Я заметил аналогичную проблему.

Я сообщил Apple Radar о похожей проблеме, которую мы заметили, # 32674764, если вы хотите сослаться на нее, если вы создаете Radar.

Я также создал тему на форуме Apple, но отзывов пока нет: https://forums.developer.apple.com/message/234654

person Chris Comeau    schedule 22.06.2017
comment
Я рад, что я не единственный, кто пострадал от этого, это означает, что, как я и ожидал, причина не в коде из моего приложения (оно существует с iOS 4, поэтому у меня были сомнения). Я продержусь еще один день, чтобы посмотреть, опубликует ли кто-нибудь прямое решение проблемы (маловероятно, и я приму ваш ответ). - person Georgi Boyadzhiev; 23.06.2017
comment
Я видел на форуме ссылку, которую Вы предоставили, что ответ от Apple будет Спасибо за ваш отзыв. Инженеры определили, что эта проблема ведет себя так, как предполагалось. Удалось ли вам добиться каких-либо успехов в раскладке кнопок в самостоятельно. - person Georgi Boyadzhiev; 12.07.2017

Я наткнулся на это: UINavigationItem-Margin. Работает как часы.

person zumph    schedule 11.03.2018

Взято из этого ответа: BarButtons теперь используют Autolayout и поэтому требуют ограничения.

if #available(iOS 9.0, *) {
  cButton.widthAnchor.constraint(equalToConstant: customViewButton.width).isActive = true
  cButton.heightAnchor.constraint(equalToConstant: customViewButton.height).isActive = true
}

Цель С

if (@available(iOS 9, *)) {
  [cButton.widthAnchor constraintEqualToConstant: standardButtonSize.width].active = YES;
  [cButton.heightAnchor constraintEqualToConstant: standardButtonSize.height].active = YES;
}
person Dane Jordan    schedule 01.10.2017
comment
Спасибо за информацию, но проблема для меня в том, что я не могу расположить кнопки, как в предыдущих версиях до iOS 11. - person Georgi Boyadzhiev; 02.10.2017

Решение 1. В свете ответа Apple о том, что это ожидаемое поведение, мы решили проблему, удалив панель инструментов и добавив настраиваемое представление.

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

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

Сделав это, мы смогли вернуться к тому же виду нашего приложения для iOS 10.

Решение 2. Немного запутанно, мы обернули нашу пользовательскую кнопку просмотра во внешний UIButton, чтобы можно было установить местоположение кадра. К сожалению, это делает внешний левый край кнопки недоступным для нажатия, но исправляет внешний вид положения кнопки. См. пример:

UIButton* outerButton = [UIButton new]; //the wrapper button
BorderedButton* button = [self initBorderedButton]; //the custom button
[button setTitle:label forState:UIControlStateNormal];
[button setFrame:CGRectMake(-10, 0, [label length] * 4 + 35, 30)];
[button addTarget:controller action:@selector(popCurrentViewController) forControlEvents:UIControlEventTouchUpInside];
[outerButton addSubview:button]; //add custom button to outer wrapper button
[outerButton setFrameWidth:button.frameWidth]; //make sure title gives button appropriate space
controller.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:outerButton]; //add wrapper button to the navigation bar
controller.navigationItem.hidesBackButton = YES;

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

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

Решение 3

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

UIBarButtonItem* backButton = [[UIBarButtonItem alloc] initWithCustomView:button];
UIBarButtonItem* spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
controller.navigationItem.leftBarButtonItems = [NSArray arrayWithObjects:backButton, spacer, nil];
person Sashah    schedule 14.08.2017
comment
но сделает ли ваше решение 2 область нажатия кнопки меньше? потому что какая-то часть button находится за пределами outerButton - person Hlung; 12.09.2017
comment
@Hlung две кнопки будут одинакового размера, поэтому это не должно влиять на область касания. - person Sashah; 12.09.2017
comment
да, они одного размера, но вы сместили начало координат button (x: -10), верно? Так что это повлияет на область касания. - person Hlung; 13.09.2017
comment
@Hlung Теперь я вижу, что ты прав. Это решение влияет на область касания для тех пользователей, которые нажимают на внешний левый край кнопки. В нашем случае использования это не проблема, но я добавлю это предостережение к решению. - person Sashah; 13.09.2017

Быстрая версия метода layoutSubviews пользовательского подкласса UINavigationBar от developer3214 (https://stackoverflow.com/a/46660888/4189037):

override func layoutSubviews() {
    super.layoutSubviews();
    if #available(iOS 11, *){
        self.layoutMargins = UIEdgeInsets()
        for subview in self.subviews {
            if String(describing: subview.classForCoder).contains("ContentView") {
                let oldEdges = subview.layoutMargins
                subview.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: oldEdges.right)
            }
        }
    }
}
person Predrag Samardzic    schedule 07.11.2017

Я была такая же проблема. У меня было три кнопки в качестве элементов правой панели в представлении стека панели навигации. Итак, я обновил вставки подвидов панели навигации.

override func viewWillLayoutSubviews() {
                super.viewWillLayoutSubviews()
                for view in (self.navigationController?.navigationBar.subviews)! {
                    view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11)
                }
            }

Здесь 11 - это пространство, которое мне нужно. Это может быть что угодно в соответствии с вашими требованиями. Кроме того, если вы хотите, чтобы кнопки были без вставок, замените view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11) на view.layoutMargins = UIEdgeInsets.zero.

person vinny    schedule 17.09.2018

Другой ответ может помочь.

Мое решение: оно работает в iOS 9–12. Вы должны вызвать fixNavigationItemsMargin(margin:) в функции viewDidAppear(_animated: Bool) и viewDidLayoutSubviews()< /сильный>. fixNavigationItemsMargin(margin:) изменит стек UINavigationController.

вы можете вызвать fixNavigationItemsMargin(margin:) в BaseNavigationController, выполняя обычную работу. И вызовите fixNavigationItemsMargin(margin:) в UIViewController для точного макета.

// do common initilizer
class BaseNavigationController: UINavigationController {
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    fixNavigationItemsMargin()
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    fixNavigationItemsMargin()
}
}

// do precise layout
class ViewController: UIViewController {
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    navigationController?.fixNavigationItemsMargin(40)
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    navigationController?.fixNavigationItemsMargin(40)
}
}

extension UINavigationController {
func fixNavigationItemsMargin(_ margin: CGFloat = 8) {
    let systemMajorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
    if systemMajorVersion >= 11 {
        // iOS >= 11
        guard let contentView = navigationBar.subviews
            .first(
                where: { sub in
                    String(describing: sub).contains("ContentView")
        }) else { return }

        // refer to: https://www.matrixprojects.net/p/uibarbuttonitem-ios11/
        // if rightBarButtonItems has not any custom views, then margin would be 8(320|375)/12(414)
        // should use customView
        let needAdjustRightItems: Bool
        if let currentVC = viewControllers.last,
            let rightItems = currentVC.navigationItem.rightBarButtonItems,
            rightItems.count > 0,
            rightItems.filter({ $0.customView != nil }).count > 0 {
            needAdjustRightItems = true
        } else {
            print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
            needAdjustRightItems = false
        }

        let needAdjustLeftItems: Bool
        if let currentVC = viewControllers.last,
            let leftItems = currentVC.navigationItem.leftBarButtonItems,
            leftItems.count > 0,
            leftItems.filter({ $0.customView != nil }).count > 0 {
            needAdjustLeftItems = true
        } else {
            print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
            needAdjustLeftItems = false
        }

        let layoutMargins: UIEdgeInsets
        if #available(iOS 11.0, *) {
            let directionInsets = contentView.directionalLayoutMargins
            layoutMargins = UIEdgeInsets(
                top: directionInsets.top,
                left: directionInsets.leading,
                bottom: directionInsets.bottom,
                right: directionInsets.trailing)
        } else {
            layoutMargins = contentView.layoutMargins
        }

        contentView.constraints.forEach(
            { cst in

                // iOS 11 the distance between rightest item and NavigationBar should be  margin
                // rightStackView trailing space is -margin / 2
                // rightestItem trailing to rightStackView trailing is -margin / 2
                let rightConstant = -margin / 2

                switch (cst.firstAttribute, cst.secondAttribute) {
                case (.leading, .leading), (.trailing, .trailing):

                    if let stackView = cst.firstItem as? UIStackView,
                        stackView.frame.minX < navigationBar.frame.midX {
                        // is leftItems
                        if needAdjustLeftItems {
                            cst.constant = margin - layoutMargins.left
                        }
                    } else if let layoutGuide = cst.firstItem as? UILayoutGuide,
                        layoutGuide.layoutFrame.minX < navigationBar.frame.midX {
                        // is leftItems
                        if needAdjustLeftItems {
                            cst.constant = margin - layoutMargins.left
                        }
                    }

                    if let stackView = cst.firstItem as? UIStackView,
                        stackView.frame.maxX > navigationBar.frame.midX {
                        // is rightItems
                        if needAdjustRightItems {
                            cst.constant = rightConstant
                        }
                    } else if let layoutGuide = cst.firstItem as? UILayoutGuide,
                        layoutGuide.layoutFrame.maxX > navigationBar.frame.midX {
                        // is rightItems
                        if needAdjustRightItems {
                            cst.constant = rightConstant
                        }
                    }

                default: break
                }

        })

        // ensure items space == 8, minispcae
        contentView.subviews.forEach(
            { subsub in
                guard subsub is UIStackView else { return }
                subsub.constraints.forEach(
                    { cst in
                        guard cst.firstAttribute == .width
                            || cst.secondAttribute == .width
                        else { return }
                        cst.constant = 0
                })
        })

    } else {
        // iOS < 11

        let versionItemsCount: Int
        if systemMajorVersion == 10 {
            // iOS 10 navigationItem.rightBarButtonItems == 0
            // space = 16(320|375) / 20(414)
            // should adjust margin
            versionItemsCount = 0
        } else {
            // iOS 9 navigationItem.rightBarButtonItems == 0
            // space = 8(320|375) / 12(414)
            // should not adjust margin
            versionItemsCount = 1
        }

        let spaceProducer = { () -> UIBarButtonItem in
            let spaceItem = UIBarButtonItem(
                barButtonSystemItem: .fixedSpace,
                target: nil,
                action: nil)
            spaceItem.width = margin - 16
            return spaceItem
        }
        if let currentVC = viewControllers.last,
            var rightItems = currentVC.navigationItem.rightBarButtonItems,
            rightItems.count > versionItemsCount,
            let first = rightItems.first {
            // ensure the first BarButtonItem is NOT fixedSpace
            if first.title == nil && first.image == nil && first.customView == nil {
                print("rightBarButtonItems SPACE SETTED!!!  SPACE: ", abs(first.width))

            } else {
                rightItems.insert(spaceProducer(), at: 0)

                // arranged right -> left
                currentVC.navigationItem.rightBarButtonItems = rightItems
            }
        }

        if let currentVC = viewControllers.last,
            var leftItems = currentVC.navigationItem.leftBarButtonItems,
            leftItems.count > versionItemsCount,
            let first = leftItems.first {
            if first.title == nil && first.image == nil && first.customView == nil {
                print("leftBarButtonItems  SPACE SETTED!!!  SPACE: ", abs(first.width))
            } else {
                leftItems.insert(spaceProducer(), at: 0)

                // arranged left -> right
                currentVC.navigationItem.leftBarButtonItems = leftItems
            }
        }
    }
}
}
person Jules    schedule 08.11.2018