web-dev-qa-db-fra.com

personnalisation de la hauteur de la barre de navigation iOS 11

Désormais dans iOS 11, la méthode sizeThatFits n'est pas appelée à partir des sous-classes UINavigationBar. Changer le cadre de UINavigationBar provoque des problèmes et des encarts incorrects . Alors, des idées pour personnaliser la hauteur de la barre de navigation maintenant?

Selon les développeurs Apple (regardez ici , ici et ici ), la modification de la hauteur de la barre de navigation dans iOS 11 n'est pas prise en charge. Ici ils suggèrent de faire des solutions de contournement comme avoir une vue sous la barre de navigation (mais en dehors de celle-ci) et ensuite supprimer la bordure de la barre de navigation. En conséquence, vous aurez ceci dans le storyboard:

 enter image description here

ressembler à ceci sur l'appareil:

 enter image description here

Vous pouvez maintenant utiliser une solution de contournement suggérée dans les autres réponses: créez une sous-classe personnalisée de UINavigationBar, ajoutez-y votre grande sous-vue personnalisée, remplacez sizeThatFits et layoutSubviews, puis définissez additionalSafeAreaInsets.top pour le contrôleur supérieur de navigation sur la différence customHeight - 44px, mais la barre La vue restera toujours la 44px par défaut, même si visuellement tout sera parfait. Je n'ai pas essayé de remplacer setFrame. Peut-être que cela fonctionne, cependant, comme l'a écrit le développeur Apple dans l'un des liens ci-dessus: "... et [n'est pas pris en charge] de changer le cadre d'une barre de navigation appartenant à un UINavigationController (le contrôleur de navigation sautillera volontiers sur vos modifications de trame chaque fois qu'il le jugera utile). "

Dans mon cas, la solution de contournement ci-dessus créait des vues ayant l'aspect suivant (vue de débogage pour afficher les bordures):

 enter image description here

Comme vous pouvez le constater, l’aspect visuel est assez bon, la additionalSafeAreaInsets a correctement poussé le contenu vers le bas, la grande barre de navigation est visible, mais j’ai un bouton personnalisé dans cette barre et seule la zone située sous la barre de navigation standard de 44 pixels est cliquable (zone verte dans l'image). Les touches situées sous la hauteur standard de la barre de navigation n'atteignent pas ma sous-vue personnalisée. J'ai donc besoin que la barre de navigation elle-même soit redimensionnée, ce qui, selon les développeurs Apple, n'est pas pris en charge.

25
frangulyan

Mis à jour le 07 janvier 2018

Ce code est supporté par XCode 9.2, iOS 11.2

J'ai eu le même problème. Ci-dessous ma solution. Je suppose que la taille taille est 66. 

S'il vous plaît choisir ma réponse si cela vous aide.

Créer CINavgationBar.Swift

   import UIKit

@IBDesignable
class CINavigationBar: UINavigationBar {

    //set NavigationBar's height
    @IBInspectable var customHeight : CGFloat = 66

    override func sizeThatFits(_ size: CGSize) -> CGSize {

        return CGSize(width: UIScreen.main.bounds.width, height: customHeight)

    }

    override func layoutSubviews() {
        super.layoutSubviews()

        print("It called")

        self.tintColor = .black
        self.backgroundColor = .red



        for subview in self.subviews {
            var stringFromClass = NSStringFromClass(subview.classForCoder)
            if stringFromClass.contains("UIBarBackground") {

                subview.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: customHeight)

                subview.backgroundColor = .green
                subview.sizeToFit()
            }

            stringFromClass = NSStringFromClass(subview.classForCoder)

            //Can't set height of the UINavigationBarContentView
            if stringFromClass.contains("UINavigationBarContentView") {

                //Set Center Y
                let centerY = (customHeight - subview.frame.height) / 2.0
                subview.frame = CGRect(x: 0, y: centerY, width: self.frame.width, height: subview.frame.height)
                subview.backgroundColor = .yellow
                subview.sizeToFit()

            }
        }


    }


}

Définir le storyboard

 enter image description here

 Set NavigationBar class

Définir une classe de barre de navigation personnalisée

 Add TestView

 enter image description here

Ajouter TestView + Set SafeArea

ViewController.Swift

import UIKit

class ViewController: UIViewController {

    var navbar : UINavigationBar!

    @IBOutlet weak var testView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        //update NavigationBar's frame
        self.navigationController?.navigationBar.sizeToFit()
        print("NavigationBar Frame : \(String(describing: self.navigationController!.navigationBar.frame))")

    }

    //Hide Statusbar
    override var prefersStatusBarHidden: Bool {

        return true
    }

    override func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(false)

        //Important!
        if #available(iOS 11.0, *) {

            //Default NavigationBar Height is 44. Custom NavigationBar Height is 66. So We should set additionalSafeAreaInsets to 66-44 = 22
            self.additionalSafeAreaInsets.top = 22

        }

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

SecondViewController.Swift

import UIKit

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.


        // Create BackButton
        var backButton: UIBarButtonItem!
        let backImage = imageFromText("Back", font: UIFont.systemFont(ofSize: 16), maxWidth: 1000, color:UIColor.white)
        backButton = UIBarButtonItem(image: backImage, style: UIBarButtonItemStyle.plain, target: self, action: #selector(SecondViewController.back(_:)))

        self.navigationItem.leftBarButtonItem = backButton
        self.navigationItem.leftBarButtonItem?.setBackgroundVerticalPositionAdjustment(-10, for: UIBarMetrics.default)


    }
    override var prefersStatusBarHidden: Bool {

        return true
    }
    @objc func back(_ sender: UITabBarItem){

        self.navigationController?.popViewController(animated: true)

    }


    //Helper Function : Get String CGSize
    func sizeOfAttributeString(_ str: NSAttributedString, maxWidth: CGFloat) -> CGSize {
        let size = str.boundingRect(with: CGSize(width: maxWidth, height: 1000), options:(NSStringDrawingOptions.usesLineFragmentOrigin), context:nil).size
        return size
    }


    //Helper Function : Convert String to UIImage
    func imageFromText(_ text:NSString, font:UIFont, maxWidth:CGFloat, color:UIColor) -> UIImage
    {
        let paragraph = NSMutableParagraphStyle()
        paragraph.lineBreakMode = NSLineBreakMode.byWordWrapping
        paragraph.alignment = .center // potentially this can be an input param too, but i guess in most use cases we want center align

        let attributedString = NSAttributedString(string: text as String, attributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.paragraphStyle:paragraph])

        let size = sizeOfAttributeString(attributedString, maxWidth: maxWidth)
        UIGraphicsBeginImageContextWithOptions(size, false , 0.0)
        attributedString.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }




    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }



}

 enter image description here  enter image description here

Le jaune est barbackgroundView. L'opacité noire est BarContentView.

Et j'ai enlevé backgroundColor de BarContentView.

 enter image description here

C'est tout.

20
Shawn Baek

Ajouté: Le problème est résolu dans iOS 11 beta 6, le code ci-dessous est donc inutile ^ _ ^


Réponse originale:

Résolu avec le code ci-dessous:

(Je veux toujours que navigationBar.height + statusBar.height == 64, que le caché de statusBar soit vrai ou non)

 @implementation P1AlwaysBigNavigationBar

- (CGSize)sizeThatFits:(CGSize)size {
    CGSize sizeThatFit = [super sizeThatFits:size];
    if ([UIApplication sharedApplication].isStatusBarHidden) {
        if (sizeThatFit.height < 64.f) {
            sizeThatFit.height = 64.f;
        }
    }
    return sizeThatFit;
}

- (void)setFrame:(CGRect)frame {
    if ([UIApplication sharedApplication].isStatusBarHidden) {
        frame.size.height = 64;
    }
    [super setFrame:frame];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (![UIApplication sharedApplication].isStatusBarHidden) {
        return;
    }

    for (UIView *subview in self.subviews) {
        NSString* subViewClassName = NSStringFromClass([subview class]);
        if ([subViewClassName containsString:@"UIBarBackground"]) {
            subview.frame = self.bounds;
        }else if ([subViewClassName containsString:@"UINavigationBarContentView"]) {
            if (subview.height < 64) {
                subview.y = 64 - subview.height;
            }else {
                subview.y = 0;
            }
        }
    }
}
@end
9
CharlieSu

cela fonctionne pour moi:

- (CGSize)sizeThatFits:(CGSize)size {
    CGSize sizeThatFit = [super sizeThatFits:size];
    if ([UIApplication sharedApplication].isStatusBarHidden) {
        if (sizeThatFit.height < 64.f) {
            sizeThatFit.height = 64.f;
        }
    }
    return sizeThatFit;
}

- (void)setFrame:(CGRect)frame {
    if ([UIApplication sharedApplication].isStatusBarHidden) {
        frame.size.height = 64;
    }
    [super setFrame:frame];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    for (UIView *subview in self.subviews) {
        if ([NSStringFromClass([subview class]) containsString:@"BarBackground"]) {
            CGRect subViewFrame = subview.frame;
            subViewFrame.Origin.y = 0;
            subViewFrame.size.height = 64;
            [subview setFrame: subViewFrame];
        }
        if ([NSStringFromClass([subview class]) containsString:@"BarContentView"]) {
            CGRect subViewFrame = subview.frame;
            subViewFrame.Origin.y = 20;
            subViewFrame.size.height = 44;
            [subview setFrame: subViewFrame];
        }
    }
}
8
Minster.Zo

Simplifié avec Swift 4.

class CustomNavigationBar : UINavigationBar {

    private let hiddenStatusBar: Bool

    // MARK: Init
    init(hiddenStatusBar: Bool = false) {
        self.hiddenStatusBar = hiddenStatusBar
        super.init(frame: .zero)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: Overrides
    override func layoutSubviews() {
        super.layoutSubviews()

        if #available(iOS 11.0, *) {
            for subview in self.subviews {
                let stringFromClass = NSStringFromClass(subview.classForCoder)
                if stringFromClass.contains("BarBackground") {
                    subview.frame = self.bounds
                } else if stringFromClass.contains("BarContentView") {
                    let statusBarHeight = self.hiddenStatusBar ? 0 : UIApplication.shared.statusBarFrame.height
                    subview.frame.Origin.y = statusBarHeight
                    subview.frame.size.height = self.bounds.height - statusBarHeight
                }
            }
        }
    }
}
4
Peymankh

En plus de remplacer -layoutSubviews et -setFrame:, vous devriez vérifier la propriété additionalSafereaInsets de UIViewController récemment ajoutée ( Apple Documentation ) si vous ne souhaitez pas que la barre de navigation redimensionnée masque votre contenu.

4
Reggian

Bien que cela soit corrigé dans la version bêta 4, il semble que l'image d'arrière-plan de la barre de navigation ne soit pas adaptée à la vue réelle (vous pouvez le vérifier en regardant dans la visionneuse de hiérarchie de vues). Une solution de contournement consiste à remplacer layoutSubviews dans votre UINavigationBar personnalisée, puis à utiliser ce code:

- (void)layoutSubviews
{
  [super layoutSubviews];

  for (UIView *subview in self.subviews) {
    if ([NSStringFromClass([subview class]) containsString:@"BarBackground"]) {
        CGRect subViewFrame = subview.frame;
        subViewFrame.Origin.y = -20;
        subViewFrame.size.height = CUSTOM_FIXED_HEIGHT+20;
        [subview setFrame: subViewFrame];
    }
  }
}

Si vous remarquez que l'arrière-plan de la barre a en fait un décalage de -20 pour le faire apparaître derrière la barre d'état, le calcul ci-dessus l'ajoute donc en.

4
strangetimes

sur Xcode 9 Beta 6, j'ai toujours le problème. La barre a toujours une hauteur de 44 pixels et est poussée sous la barre d'état.

Afin de résoudre ce problème, j’ai créé une sous-classe avec le code @strangetimes (dans Swift)

class NavigationBar: UINavigationBar {

  override func layoutSubviews() {
    super.layoutSubviews()

    for subview in self.subviews {
      var stringFromClass = NSStringFromClass(subview.classForCoder)
      print("--------- \(stringFromClass)")
      if stringFromClass.contains("BarBackground") {
        subview.frame.Origin.y = -20
        subview.frame.size.height = 64
      }
    }
  }
}

et je place la barre plus bas que la barre d'état

let newNavigationBar = NavigationBar(frame: CGRect(Origin: CGPoint(x: 0,
                                                                       y: 20),
                                                         size: CGSize(width: view.frame.width,
                                                                      height: 64)
      )
    ) 
3
Marco Pappalardo

C'est ce que j'utilise. Cela fonctionne pour le contenu normal (44,0 px) si vous utilisez UISearchBar comme titre ou toute autre vue modifiant la taille du contenu de la barre, vous devez mettre à jour les valeurs en conséquence. Utilisez-le à vos risques et périls car il pourrait freiner à un moment donné.

Il s'agit de la barre de navigation avec une hauteur de 90.0px codée en dur, fonctionnant à la fois sur iOS 11 et les versions antérieures. Il se peut que vous deviez ajouter des incrustations à la UIBarButtonItem pour les versions antérieures à iOS 11 pour qu'elles soient identiques.

class NavBar: UINavigationBar {

    override init(frame: CGRect) {
        super.init(frame: frame)

        if #available(iOS 11, *) {
            translatesAutoresizingMaskIntoConstraints = false
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        return CGSize(width: UIScreen.main.bounds.width, height: 70.0)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        guard #available(iOS 11, *) else {
            return
        }

        frame = CGRect(x: frame.Origin.x, y:  0, width: frame.size.width, height: 90)

        if let parent = superview {
            parent.layoutIfNeeded()

            for view in parent.subviews {
                let stringFromClass = NSStringFromClass(view.classForCoder)
                if stringFromClass.contains("NavigationTransition") {
                    view.frame = CGRect(x: view.frame.Origin.x, y: frame.size.height - 64, width: view.frame.size.width, height: parent.bounds.size.height - frame.size.height + 4)
                }
            }
        }

        for subview in self.subviews {
            var stringFromClass = NSStringFromClass(subview.classForCoder)
            if stringFromClass.contains("BarBackground") {
                subview.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: 90)
                subview.backgroundColor = .yellow
            }

            stringFromClass = NSStringFromClass(subview.classForCoder)
            if stringFromClass.contains("BarContent") {
                subview.frame = CGRect(x: subview.frame.Origin.x, y: 40, width: subview.frame.width, height: subview.frame.height)

            }
        }
    }
}

Et vous l'ajoutez à une sous-classe UINavigationController comme ceci:

class CustomBarNavigationViewController: UINavigationController {

    init() {
        super.init(navigationBarClass: NavBar.self, toolbarClass: nil)
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    override init(rootViewController: UIViewController) {
        super.init(navigationBarClass: NavBar.self, toolbarClass: nil)

        self.viewControllers = [rootViewController]
    }

    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}
2
Jelly

Je doublais la hauteur de ma barre de navigation pour pouvoir ajouter une rangée d'icônes d'état au-dessus des contrôles de navigation par défaut, en sous-classant UINavigationBar et en utilisant sizeThatFits pour remplacer la hauteur. Heureusement, cela a le même effet, et est plus simple, avec moins d'effets secondaires. Je l'ai testé avec iOS 8 à 11. Mettez ceci dans votre contrôleur de vue:

- (void)viewDidLoad {
    [super viewDidLoad];
    if (self.navigationController) {
        self.navigationItem.Prompt = @" "; // this adds empty space on top
    }
}
0
arlomedia

Cela fonctionne bien pour la barre de navigation normale. Si vous utilisez LargeTitle, cela ne fonctionnera pas, car la taille de titleView ne sera pas une hauteur fixe de 44 points. Mais pour la vue habituelle, cela devrait suffire.

Comme @frangulyan, Apple a suggéré d'ajouter une vue sous la barre de navigation et de masquer la fine ligne (image ombrée). C'est ce que je suis venu avec ci-dessous. J'ai ajouté un uiview au titleView de navigationItem, puis un imageView à l'intérieur de cet uiview. J'ai enlevé la ligne mince (image d'ombre). Le uiview que j'ai ajouté est la même couleur exacte que le navBar . J'ai ajouté un uiLabel à l'intérieur de cette vue et c'est tout.

 enter image description here 

Voici l'image 3d. La vue étendue est derrière le nom d'utilisateurLabel sous le navBar. Il est gris et a une fine ligne en dessous. Il suffit d’ancrer votre collection. Voir ce que vous avez sous la fine ligne separator.

 enter image description here 

Les 9 étapes sont expliquées au-dessus de chaque ligne de code:

class ExtendedNavController: UIViewController {

    fileprivate let extendedView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .white
        return view
    }()

    fileprivate let separatorLine: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .gray
        return view
    }()

    fileprivate let usernameLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.systemFont(ofSize: 14)
        label.text = "username goes here"
        label.textAlignment = .center
        label.lineBreakMode = .byTruncatingTail
        label.numberOfLines = 1
        return label
    }()

    fileprivate let myTitleView: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        return view
    }()

    fileprivate let profileImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.clipsToBounds = true
        imageView.backgroundColor = .darkGray
        return imageView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        // 1. the navBar's titleView has a height of 44, set myTitleView height and width both to 44
        myTitleView.frame = CGRect(x: 0, y: 0, width: 44, height: 44)

        // 2. set myTitleView to the nav bar's titleView
        navigationItem.titleView = myTitleView

        // 3. get rid of the thin line (shadow Image) underneath the navigationBar
        navigationController?.navigationBar.setValue(true, forKey: "hidesShadow")
        navigationController?.navigationBar.layoutIfNeeded()

        // 4. set the navigationBar's tint color to the color you want
        navigationController?.navigationBar.barTintColor = UIColor(red: 249.0/255.0, green: 249.0/255.0, blue: 249.0/255.0, alpha: 1.0)

        // 5. set extendedView's background color to the same exact color as the navBar's background color
        extendedView.backgroundColor = UIColor(red: 249.0/255.0, green: 249.0/255.0, blue: 249.0/255.0, alpha: 1.0)

        // 6. set your imageView to get pinned inside the titleView
        setProfileImageViewAnchorsInsideMyTitleView()

        // 7. set the extendedView's anchors directly underneath the navigation bar
        setExtendedViewAndSeparatorLineAnchors()

        // 8. set the usernameLabel's anchors inside the extendedView
        setNameLabelAnchorsInsideTheExtendedView()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(true)

        // 9. **Optional** If you want the shadow image to show on other view controllers when popping or pushing
        navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        navigationController?.navigationBar.setValue(false, forKey: "hidesShadow")
        navigationController?.navigationBar.layoutIfNeeded()
    }

    func setExtendedViewAndSeparatorLineAnchors() {

        view.addSubview(extendedView)
        view.addSubview(separatorLine)

        extendedView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        extendedView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        extendedView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        extendedView.heightAnchor.constraint(equalToConstant: 29.5).isActive = true

        separatorLine.topAnchor.constraint(equalTo:  extendedView.bottomAnchor).isActive = true
        separatorLine.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        separatorLine.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        separatorLine.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
    }

    func setProfileImageViewAnchorsInsideMyTitleView() {

        myTitleView.addSubview(profileImageView)

        profileImageView.topAnchor.constraint(equalTo: myTitleView.topAnchor).isActive = true
        profileImageView.centerXAnchor.constraint(equalTo: myTitleView.centerXAnchor).isActive = true
        profileImageView.widthAnchor.constraint(equalToConstant: 44).isActive = true
        profileImageView.heightAnchor.constraint(equalToConstant: 44).isActive = true

        // round the profileImageView
        profileImageView.layoutIfNeeded()
        profileImageView.layer.cornerRadius = profileImageView.frame.width / 2
    }

    func setNameLabelAnchorsInsideTheExtendedView() {

        extendedView.addSubview(usernameLabel)

        usernameLabel.topAnchor.constraint(equalTo: extendedView.topAnchor).isActive = true
        usernameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        usernameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    }
}
0
Lance Samaria