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:
ressembler à ceci sur l'appareil:
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):
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.
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
Définir une classe de barre de navigation personnalisée
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.
}
}
Le jaune est barbackgroundView. L'opacité noire est BarContentView.
Et j'ai enlevé backgroundColor de BarContentView.
C'est tout.
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
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];
}
}
}
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
}
}
}
}
}
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.
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.
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)
)
)
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")
}
}
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
}
}
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.
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.
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
}
}