Skip to content

Instantly share code, notes, and snippets.

@niw
Last active January 3, 2024 11:35
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save niw/569b49648fcab22124e1d12c195fe595 to your computer and use it in GitHub Desktop.
Save niw/569b49648fcab22124e1d12c195fe595 to your computer and use it in GitHub Desktop.
A note of my observation about iOS 11 UINavigationBar behavior.

UINavigationBar on iOS 11

NOTE This note is written based on Xcode version 9.0 beta 6 (9M214v) and its simulator binary.

iOS 11 changes UINavigationBar a lot, not just only for its large title, but also the internal view hierarchy and lay outing views are changed. This is a small note about UINavigationBar behavior on iOS 11, mainly focusing on migrating the application to iOS 11.

Lay outing views

UINavigationBar has been using manual lay outing until iOS 10, so all its content views like titleView has been directly child view of the UINavigationBar. However, since iOS 11, it is using auto layout with bunch of layout guides to lay out its content views in its own internal container view, _UINavigationBarContentView.

The view hierarchy loosk like this.

UINavigationBar
  | _UIBarBackground
  |    | UIImageView
  |    | UIVisualEffectView
  |    |    | _UIVisualEffectBackdropView
  |    |    | _UIVisualEffectSubview
  | _UINavigationBarLargeTitleView
  |    | UILabel
  | _UINavigationBarContentView // ← All content views goes in here.
  |    | // There are many layout guieds for title view, buttons etc.
  |    | _UIButtonBarStackView
  |    |    | _UIButtonBarButton
  |    |    |    | _UIModernBarButton
  |    |    |    |    | UIButtonLabel
  |    | _UITAMICAdaptorView // ← Only when `titleView` doesn't have `intricinsicSize`.
  |    |    | MyTitleView // ← This is where your `titleView` goes.
  | _UINavigationBarModernPromptView
  |    | UILabel

Because of this, the titleView which deson't support auto layout, in another words, doesn't have intrinsicContentSize, will be encupsulated by the internal adapter view (_UITAMICAdaptorView) to make it works in auto layout environment. So, the traditional lay outing — like assigning a frame same as UINavigationBar bounds and set autolayoutMask is no longer working, instead, the titleView needs to have an explicit intrinsicContentSize or sizeThatFits: to tell that adapter view to give a right frame to it.

A simple implementation that enforces the titleView to fit entier title view area can be done by adding next to it.

- (CGSize)intrinsigContentSize
{
    return CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
}

Using setLeftBarButtonItem:animated: or setRightBarButtonItem:animated: may have a trouble

By disassembling current UIKit binary, looks like setLeftBarButtonItem:animated: or setRightBarButtonItem:animated: call an internal lay outing code somehow, twice, and it may cause a trouble with buttons.

To workaround this behavior, always use setLeftBarButtonItems:animated: or setRightBarButtonItem:animated: with @[barButtonItem] instead. I think it's an iOS 11 implementation bug.

No animations for UIBarButtonItem

These setLeftBarButtonItem:animated:, setRightBarButtonItem:animated:, setLeftBarButtonItems:animated:, and setRightBarButtonItems:animated: are not really animating items at all.

I think it's also an iOS 11 implementation bug, because in the chain of call from these mtehods are evenatually calling updateTopNavigationItemAnimated:, however, the current implementation is like this.

-[_UINavigationBarVisualProviderModernIOS updateTopNavigationItemAnimated:]:
0000000000be5708         push       rbp
; Objective C Implementation defined at 0x13ff050 (instance method), DATA XREF=0x13ff050
0000000000be5709         mov        rbp, rsp
0000000000be570c         mov        rsi, qword [0x147f030]
; @selector(setupTopNavigationItem), argument "selector" for method _objc_msgSend
0000000000be5713         pop        rbp
0000000000be5714         jmp        qword [_objc_msgSend_11195c8]
; _objc_msgSend

Obvisouly, this method ignores given animated argument and simpley calls setupTopNavigationItem which doesn't take any arugments. Thus, animated flag is ignored at this point.

Because of this implementation, seems like, in some cases, UIBarButton sometimes remains pressed state.

@dkulundzic
Copy link

dkulundzic commented Jul 16, 2018

So, the traditional lay outing — like assigning a frame same as UINavigationBar bounds and set autolayoutMask is no longer working, instead, the titleView needs to have an explicit intrinsicContentSize or sizeThatFits: to tell that adapter view to give a right frame to it.

  • This was the missing piece of the puzzle to get the titleView to play nicely with Auto Layout. Thanks!

@BrettJackson1987
Copy link

BrettJackson1987 commented Nov 3, 2018

I cheated with a workaround to fade OUT the navigation bar items I had, but this code may not be App Store safe. The context of this app is that a user can tap the space below the navigation bar to fade in/out the navigation bar and toolbar.

let doneButtonItemView = self.doneButtonItem.value(forKey: "view") as? UIView
let shareButtonItemView = self.shareButtonItem.value(forKey: "view") as? UIView

let buttonItemsViews = [doneButtonItemView, shareButtonItemView]

UIView.animate(withDuration: 0.2, animations: {
    buttonItemsViews.forEach { $0?.alpha = 0.0 }
}) { _ in
    self.navigationItem.setLeftBarButtonItems(nil, animated: false)
    self.navigationItem.setRightBarButtonItems(nil, animated: false)
    
    buttonItemsViews.forEach { $0?.alpha = 1.0 }
}

@maniak-dobrii
Copy link

You have a typo, - (CGSize)intrinsigContentSize method should be named intrinsicContentSize.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment