Reputation: 3791
I am attempting to animate the change to the title of a detail UITableViewController
that is embedded in a UINavigationController
.
These two Q&A on stack overflow seem most relevant...
I've attempted a number of code variants, mostly following Ashley Mills answer and others based on his answer, but essentially I cannot seem to make the animation work!
Note writing in Swift 5 using Xcode 10.2.
I'm using a Master-Detail UITableViewController
setup to manage a list and the details of the items in that list.
I'm using Large Titles...
navigationController?.navigationBar.prefersLargeTitles = true
...and a search controller...
navigationItem.searchController = <<mySearchController>>
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true
...all set in the viewDidLoad()
method for the Master View Controller.
Here's what I've done.
In my project:
import QuartzCore framework, per the comment by Jack Bellis "you'll need to add the QuartzCore framework via [project]>[target]>Build Phases>Link Binaries>QuartzCore.framework.";
In the Master View Controller:
import QuartzCore
;
In the Detail View Controller:
In the viewDidAppear(_ animated: Bool)
method, write the CATransition
code similar to the answers to the OP noted above (which changes the Large Title, but without animation).
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animationTransition = CATransition()
animationTransition.duration = 2.0
animationTransition.type = CATransitionType.push
animationTransition.subtype = CATransitionSubtype.fromTop
navigationController!.navigationBar.layer.add(animationTransition, forKey: "pushText")
navigationItem.title = <<NEW TITLE TEXT>>
}
In the viewDidAppear(_ animated: Bool)
method, write alternatives to the CATransition
code suggested in the answers to the OP noted above (which only adds a separate title into the centre of the navigation bar and does not change the Large Title).
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animationTransition = CATransition()
animationTransition.duration = 2.0
animationTransition.type = CATransitionType.fade
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 44))
label.text = <<NEW TITLE TEXT>>
navigationItem.titleView = label
navigationItem.titleView!.layer.add(animationTransition, forKey: "fadeText")
}
I've also tried including CAMediaTimingFunction
...
animationTransition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
I've also tried calling setNeedsLayout
and setNeedsDisplay
...
navigationItem.titleView?.setNeedsLayout()
navigationController?.navigationBar.setNeedsDisplay()
For clarity:
viewDidAppear(_ animated:)
method, because I want the user to see the transition;UITableView
/Controller
and UINavigationController
settings to see whether I may have missed something.Any suggestions?
Upvotes: 4
Views: 2102
Reputation: 3791
So I've worked it out.
I had to find the specific layer within the UINavigationBar
's subviews that contained the title text, then animate that layer. The results are exactly what I wanted.
Here's my answer (Swift 5 iOS 12)...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if (newTitle ?? "").isEmpty == false { // only proceed with a valid value for newTitle.
// CATransition code
let titleAnimation = CATransition()
titleAnimation.duration = 0.5
titleAnimation.type = CATransitionType.push
titleAnimation.subtype = CATransitionSubtype.fromRight
titleAnimation.timingFunction = CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.easeInEaseOut)
// this is a detail view controller, so we must grab the reference
// to the parent view controller's navigation controller
// then cycle through until we find the title labels.
if let subviews = parent?.navigationController?.navigationBar.subviews {
for navigationItem in subviews {
for itemSubView in navigationItem.subviews {
if let largeLabel = itemSubView as? UILabel {
largeLabel.layer.add(titleAnimation, forKey: "changeTitle")
}
}
}
}
// finally set the title
navigationItem.title = newTitle
}
Note: there is no need to import QuartzCore
.
...and here's the process I went through to identify what I had to change...
This SO Q&A How to set multi line Large title in navigation bar? ( New feature of iOS 11) helped me identify the process detailed below, so thanks in particular to the original post and the answer by @Krunal .
Using the same code to cycle through the UINavigationBar
's subviews (as above), I used a print to terminal to identify the various UINavigationItem
s and their subviews.
counter = 0
if let subviews = parent?.navigationController?.navigationBar.subviews {
for navigationItem in subviews {
print("____\(navigationItem)")
for itemSubView in navigationItem.subviews {
counter += 1
print("_______\(itemSubView)")
}
}
}
print("COUNTER: \(counter)")
this code yielded the following prints in terminal (for iPhone 8 Plus running iOS 12.2 in simulator)...
____<_UIBarBackground: 0x7f922740c000; frame = (0 -20; 414 116); userInteractionEnabled = NO; layer = <CALayer: 0x6000026ce1c0>>
_______<UIImageView: 0x7f922740c9c0; frame = (0 116; 414 0.333333); userInteractionEnabled = NO; layer = <CALayer: 0x6000026ce7c0>>
_______<UIVisualEffectView: 0x7f922740cbf0; frame = (0 0; 414 116); layer = <CALayer: 0x6000026ce880>>
____<_UINavigationBarLargeTitleView: 0x7f922740f390; frame = (0 44; 414 52); clipsToBounds = YES; layer = <CALayer: 0x6000026cd840>>
_______<UILabel: 0x7f9227499fe0; frame = (20.1667 3.66667; 206.333 40.6667); text = 'Event Details'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000005bf250>>
____<_UINavigationBarContentView: 0x7f922740d660; frame = (0 0; 414 44); clipsToBounds = YES; layer = <CALayer: 0x6000026cea00>>
_______<_UIButtonBarStackView: 0x7f92274984a0; frame = (302 0; 100 44); layer = <CALayer: 0x60000261cc60>>
_______<_UIButtonBarButton: 0x7f922749a5c0; frame = (0 0; 82.3333 44); layer = <CALayer: 0x60000261ee60>>
_______<UILabel: 0x7f922749a2d0; frame = (155 11.6667; 104.333 20.3333); text = 'Event Details'; alpha = 0; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000005bfc50>>
____<_UINavigationBarModernPromptView: 0x7f9227602db0; frame = (0 0; 0 50); alpha = 0; hidden = YES; layer = <CALayer: 0x6000026e4600>>
COUNTER: 2
I've actually applied the animation twice - to the UILabel
layer
within _UINavigationBarLargeTitleView
and the UILabel
layer
within _UINavigationBarContentView
. This does not seem to matter however because, when the large title first appears, the label within content view (which I assume is for the "old style" title in the navigation bar when the large title is scrolled off screen) is hidden on viewDidAppear
.
Incidentally, if you drop in the following two lines, you'll also have multi-line large titles:
largeLabel.numberOfLines = 0
largeLabel.lineBreakMode = .byWordWrapping
BUT, I've not yet figured out how to animate the increase in size of the large title frame, so a change to two or more lines is immediate and IMHO ruins the animation of the title change.
Not yet tested on device, but does seem to work OK for both iPhone and iPad sims.
If you find any bugs, let me know and I'll update my answer.
Upvotes: 4