In my Swift app I have this class that handles a UITabBar.
class CustomTabBar: UITabBar {
override func awakeFromNib() {
super.awakeFromNib()
}
}
How can I animate the items when the user tap their?
I mean a CGAffine(scaleX: 1.1, y: 1.1)
So how I can animate the tab bar's items?
First: create a custom UITabBarController as follows:
import UIKit
enum TabbarItemTag: Int {
case firstViewController = 101
case secondViewConroller = 102
}
class CustomTabBarController: UITabBarController {
var firstTabbarItemImageView: UIImageView!
var secondTabbarItemImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let firstItemView = tabBar.subviews.first!
firstTabbarItemImageView = firstItemView.subviews.first as? UIImageView
firstTabbarItemImageView.contentMode = .center
let secondItemView = self.tabBar.subviews[1]
self.secondTabbarItemImageView = secondItemView.subviews.first as? UIImageView
self.secondTabbarItemImageView.contentMode = .center
}
private func animate(_ imageView: UIImageView) {
UIView.animate(withDuration: 0.1, animations: {
imageView.transform = CGAffineTransform(scaleX: 1.25, y: 1.25)
}) { _ in
UIView.animate(withDuration: 0.25, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 3.0, options: .curveEaseInOut, animations: {
imageView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: nil)
}
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let tabbarItemTag = TabbarItemTag(rawValue: item.tag) else {
return
}
switch tabbarItemTag {
case .firstViewController:
animate(firstTabbarItemImageView)
case .secondViewConroller:
animate(secondTabbarItemImageView)
}
}
}
Second: Set the tag values for the tabBarItem for each view controller:
First ViewController:
import UIKit
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
tabBarItem.tag = TabbarItemTag.firstViewController.rawValue
}
}
Second ViewController:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
tabBarItem.tag = TabbarItemTag.secondViewConroller.rawValue
}
}
Make sure that everything has been setup with your storyboard (if you are using one) and that's pretty much it!
Output:
You could check the repo:
https://github.com/AhmadFayyas/Animated-TabbarItem/tree/master
for demonstrating the answer.
This do the trick for me:
class MyCustomTabController: UITabBarController {
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let barItemView = item.value(forKey: "view") as? UIView else { return }
let timeInterval: TimeInterval = 0.3
let propertyAnimator = UIViewPropertyAnimator(duration: timeInterval, dampingRatio: 0.5) {
barItemView.transform = CGAffineTransform.identity.scaledBy(x: 0.9, y: 0.9)
}
propertyAnimator.addAnimations({ barItemView.transform = .identity }, delayFactor: CGFloat(timeInterval))
propertyAnimator.startAnimation()
}
}
As UITabBarItem is not a UIView subclass, but an NSObject subclass instead, there is no direct way to animate an item when tapped.
You either have to dig up the UIView that belongs to the item and animate that, or create a custom tab bar.
Here are some ideas for digging up the UIView. And here for example how to get triggered when an item is tapped. But be very careful with this approach:
Apple may change the UITabBar implementation, which could break this.
You may interfere with iOS animations and get weird effects.
By the way, there's no need to subclass UITabBar. Implementing UITabBarDelegate is all you'd need.
I would actually advise you to just stick with the standard UITabBar behaviour & skinning options, and figure this out later or not at all. Things like this can burn your time without adding much to the app.
Related
Trying to hide both TabBar and StatusBar simultaneously and inside the same animation block, I came across an incomprehensible layout behavior. Starting to hide TabBar in the usual way with tabbar item viewcontroller:
import UIKit
class TestViewController: UIViewController {
var mainViewController: UITabBarController {
get {
return UIApplication.shared.windows.first {$0.rootViewController != nil}?.rootViewController as! UITabBarController
}
}
var offset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
offset = mainViewController.tabBar.frame.height
}
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator.startAnimation()
}
}
So far so good:
Now let's add animation for StatusBar:
import UIKit
class TestViewController: UIViewController {
var mainViewController: UITabBarController {
get {
return UIApplication.shared.windows.first {$0.rootViewController != nil}?.rootViewController as! UITabBarController
}
}
var isTabBarHidden = false {
didSet(newValue) {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
get {
return isTabBarHidden
}
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
get {
return .slide
}
}
var offset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
offset = mainViewController.tabBar.frame.height
}
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
self.isTabBarHidden = true
}
animator.startAnimation()
}
}
Now StatusBar is sliding, bur TabBar froze (I don't know why):
Any attempts to update layout using layoutIfNeeded(), setNeedsLayout() etc. were unsuccessful. Now let's swap animations for TabBar and StatusBar:
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.isTabBarHidden = true
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator.startAnimation()
}
Both are sliding now, but TabBar started to jump at the begining of animation:
I found that when adding directives for StatusBar to an animation block, a ViewDidLayoutSubviews() starts to be called additionally. Actually, you can fix the initial position of TabBar inside ViewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if isTabBarHidden {
let tabBar = self.mainViewController.tabBar
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
}
The disadvantage of this method is that TabBar can twitch during the movement, depending on the speed of movement and other factors.
Another way (without using ViewDidLayoutSubviews()) is contrary to logic but works in practice. Namely, you can put one animation in a completion block of another one:
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator1 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.isTabBarHidden = !self.isTabBarHidden
}
animator1.addCompletion({_ in
let animator2 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator2.startAnimation()
})
animator1.startAnimation()
}
Following the logic, we have two consecutive animations. And TabBar animation should begin after StatusBar animation ends. However, in practice:
The disadvantage of this method is that if you want to reverse the animation (for example, the user tapped the screen while TabBar is moving), the variable animator1.isRunning will be false, although physically the StatusBar will still move around the screen (I also don't know why).
Looking forward to reading your comments, suggestions, explanations.
The logic is that setNeedsStatusBarAppearanceUpdate() is animated asyncroniusly. I.e. StatusBar animation ends immediately after start and then runs in a thread that cannot be paused or reversed. It’s a pity that iOS SDK doesn't provide StatusBar animation control.
How to prevent the effect of setNeedsStatusBarAppearanceUpdate() animation on the layout, I still do not know.
I am trying to present modally a view when tapping a button, that would have at first the same frame than the button, and then expanding to end up full screen, all this using UIViewControllerTransitioningDelegate.
Here is my code:
Expandable Base
class ExpandableBase: UIViewController {
var senderFrame: CGRect = .zero
#IBOutlet weak var fullScreenPopupView: UIView?
#IBAction func dismiss() {
self.dismiss(animated: true, completion: nil)
}
}
Transitioning Delegate extension
extension ExpandableBase: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return ExpandableBasePresenter()
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return ExpandableBaseDismisser()
}
}
private final class ExpandableBasePresenter: NSObject, UIViewControllerAnimatedTransitioning {
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.8
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let toViewController: ExpandableBase = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! ExpandableBase
let duration = self.transitionDuration(using: transitionContext)
let containerView = transitionContext.containerView
toViewController.view.frame = containerView.frame
containerView.addSubview(toViewController.view)
let finishFrame = toViewController.fullScreenPopupView?.frame
toViewController.fullScreenPopupView?.frame = toViewController.senderFrame
UIView.animate(withDuration: duration, delay: 0.3, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: {
toViewController.fullScreenPopupView?.frame = finishFrame!
}) { result in
transitionContext.completeTransition(result)
}
}
}
private final class ExpandableBaseDismisser: NSObject, UIViewControllerAnimatedTransitioning {
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromViewController: ExpandableBase = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! ExpandableBase
let duration = self.transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0.1, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: {
fromViewController.fullScreenPopupView?.frame = fromViewController.senderFrame
}) { result in
transitionContext.completeTransition(result)
}
}
}
A simple view using this, presenting a label and a dismiss button:
final class ExpandableSimpleView: ExpandableBase {
init(from initialFrame: CGRect) {
super.init(nibName: "ExpandableSimpleView", bundle: .main)
self.senderFrame = initialFrame
self.transitioningDelegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("NOPE")
}
static func present(fromInitialFrame initialFrame: CGRect) {
let expandableSimpleView = ExpandableSimpleView(from: initialFrame)
expandableSimpleView.modalPresentationStyle = .overCurrentContext
AppDelegateTopMostViewController.present(expandableSimpleView, animated: true, completion: nil)
//AppDelegateTopMostViewController is a global reference to the top-most view controller of the app
}
}
Here is the corresponding XIB:
And how I present this from the parent view controller:
#IBAction func openSimpleView(_ sender: UIButton) {
ExpandableSimpleView.present(fromInitialFrame: sender.frame)
}
And here are some screenshots showing how this view expands:
Although the view expands fine, the label is not centered as it should be. I don't understand why.
Thank you for your help.
EDIT: following matt's answer, I have made the following changes in animateTransition's presenter.
toViewController.fullScreenPopupView!.transform =
toViewController.fullScreenPopupView!.transform.scaledBy(x: toViewController.senderFrame.width / toViewController.view.bounds.width, y: toViewController.senderFrame.height / toViewController.view.bounds.height)
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction], animations: {
toViewController.fullScreenPopupView!.transform = .identity
}, completion: nil)
Now the animation is fine, but I'm facing another issue: the button in the view is not clickable any longer.
Your animation is wrong. Do not start with a small frame and animate an expansion of the frame. Start with a small transform (e.g. x and y scale values of 0.1) and expand the transform (to .identity).
There is nothing wrong with shrinking or growing the parent view.
You don't have to animate the actual views. You can create and remove temporary views. Hide the actual view and reveal it during takedown.
I suggest that you animate the UILabel separately. It doesn't have to shrink or grow. It merely has to remain stationary. Place a temporary UILabel over the original, hide the original, and perform the animation. Reverse the process during takedown.
I'm trying to code an animation behind the profile picture, in order to encourage the user to click on it.
It's a circle which become bigger and smaller then.
override func layoutSubviews() {
super.layoutSubviews()
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.bouncedView.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
The problem is that, when I go to another viewController, the animation is stopped and the circle stays like you can the on the screen shot. Do you know how I could avoid this issue ?
Do a transform to .identity on ViewDidAppear. Something similar to below code:
class HomeController: UIViewController {
#IBOutlet weak var viewToAnimate: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.viewToAnimate.transform = .identity
animateView()
}
func animateView(){
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.viewToAnimate.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
}
The problem as you might have guessed is that the UIView.animate is only called on the ViewDidLoad method and since we don't have access that code while returning to this ViewController from another, it is better to start the animation in the ViewWillAppear method.
If the same issue occurs when you switch between tabs, then please make a separate UIView subclass for the view that you want to animate and proceed as follows:
class AnimateView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
}
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
self.transform = .identity
animateView()
}
func animateView(){
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
}
Here, you have taken the animate function to the UIView object and whenever the view appears, the animation will be reset.
Well, I don't yet have a satisfying answer yet (at least that would satisfy me), but I would like to add at least some insight to which I was able to get by a bit of experimenting.
First of all, it seems that the animation gets ended when the viewController is not currently the one presented. In your case that means that the animation stops and finishes with the state, in which the view is 1.3 times bigger, and stops repeating. layoutSubviews gets called only when you present it the first time. At least the viewController.viewDidLayoutSubviews gets called only at the beginning and not when you go back (so layoutSubviews will not get executed when the view reappears) - so the animation won't get restarted.
I tried to move the animation to the viewDidAppear of a UIViewController - that did not work either, because stopping animation resulted in state in which the view was already scaled 1.3 times. Resetting the state to CGAffineTransform.identity before creating the animation did work.
Now as I said, this can serve you as a workaround on how to get it working, however, I think what you really are looking for is some hook that would tell your view (not viewController) that it got presented again. But the following minimal example can at least help others to take a look at it and not start from scratch.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: UICubicTimingParameters(animationCurve: .linear))
init(title: String) {
super.init(nibName: nil, bundle: nil)
self.title = title
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let animatableView = UIView()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
animatableView.frame = CGRect(x: 150, y: 400, width: 100, height: 100)
animatableView.backgroundColor = UIColor.magenta
view.addSubview(animatableView)
self.view = view
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.animatableView.transform = CGAffineTransform.identity
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.animatableView.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("\(self.title) >> Lay out")
}
}
let tabBar = UITabBarController()
tabBar.viewControllers = [MyViewController(title: "first"), MyViewController(title: "second"), MyViewController(title: "third")]
tabBar.selectedIndex = 0
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = tabBar
EDIT
I added few lines to check if the self.animatableView.layer.animationKeys() contains any animations, it seems that switching tabs removes the animation from the view's layer - so you have to find a way to add the animation everytime the view reappears.
EDIT 2
So I would go with #SWAT's answer and use willMove(toWindow:) instead of layoutSubviews.
I'm trying to create a transition effect on a UITabBarController somewhat similar to the Facebook app. I managed to get a "scrolling effect" working on tab switch, but I can't seem to figure out how to cross dissolve (or it doesn't work at least).
Here's my current code:
import UIKit
class ScrollingTabBarControllerDelegate: NSObject, UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return ScrollingTransitionAnimator(tabBarController: tabBarController, lastIndex: tabBarController.selectedIndex)
}
}
class ScrollingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
weak var transitionContext: UIViewControllerContextTransitioning?
var tabBarController: UITabBarController!
var lastIndex = 0
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
init(tabBarController: UITabBarController, lastIndex: Int) {
self.tabBarController = tabBarController
self.lastIndex = lastIndex
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
containerView.addSubview(toViewController!.view)
var viewWidth = toViewController!.view.bounds.width
if tabBarController.selectedIndex < lastIndex {
viewWidth = -viewWidth
}
toViewController!.view.transform = CGAffineTransform(translationX: viewWidth, y: 0)
UIView.animate(withDuration: self.transitionDuration(using: (self.transitionContext)), delay: 0.0, usingSpringWithDamping: 1.2, initialSpringVelocity: 2.5, options: .transitionCrossDissolve, animations: {
toViewController!.view.transform = CGAffineTransform.identity
fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)
}, completion: { _ in
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
fromViewController!.view.transform = CGAffineTransform.identity
})
}
}
Would be great if anyone know how to get this to work, been trying for days now without progress... :/
edit: I got a cross dissolve working by replacing the UIView.animate block with:
UIView.transition(with: containerView, duration: 0.2, options: .transitionCrossDissolve, animations: {
toViewController!.view.transform = CGAffineTransform.identity
fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)
}, completion: { _ in
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
fromViewController!.view.transform = CGAffineTransform.identity
})
However, the animation is really laggy and not usable :(
edit 2: For people trying to use these snippets, don't forget to hook up the delegate for the UITabBarController, otherwise nothing will happen.
edit 3: I've found a Swift library that does exactly what I was looking for:
https://github.com/Interactive-Studio/TransitionableTab
There is a simpler way to doing this. Add the following code in the tabbar delegate:
Working on Swift 2, 3 and 4
class MySubclassedTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MySubclassedTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view, let toView = viewController.view else {
return false // Make sure you want this as false
}
if fromView != toView {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: nil)
}
return true
}
}
EDIT (4/23/18)
Since this answer is getting popular, I updated the code to remove the force unwraps, which is a bad practice, and added the guard statement.
EDIT (7/11/18)
#AlbertoGarcĂa is right. If you tap the tabbar icon twice you get a blank screen. So I added an extra check
If you want to use UIViewControllerAnimatedTransitioning to do something more custom than UIView.transition, take a look at this gist.
// MyTabController.swift
import UIKit
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return MyTransition(viewControllers: tabBarController.viewControllers)
}
}
class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {
let viewControllers: [UIViewController]?
let transitionDuration: Double = 1
init(viewControllers: [UIViewController]?) {
self.viewControllers = viewControllers
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(transitionDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let fromView = fromVC.view,
let fromIndex = getIndex(forViewController: fromVC),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let toView = toVC.view,
let toIndex = getIndex(forViewController: toVC)
else {
transitionContext.completeTransition(false)
return
}
let frame = transitionContext.initialFrame(for: fromVC)
var fromFrameEnd = frame
var toFrameStart = frame
fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width
toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width
toView.frame = toFrameStart
DispatchQueue.main.async {
transitionContext.containerView.addSubview(toView)
UIView.animate(withDuration: self.transitionDuration, animations: {
fromView.frame = fromFrameEnd
toView.frame = frame
}, completion: {success in
fromView.removeFromSuperview()
transitionContext.completeTransition(success)
})
}
}
func getIndex(forViewController vc: UIViewController) -> Int? {
guard let vcs = self.viewControllers else { return nil }
for (index, thisVC) in vcs.enumerated() {
if thisVC == vc { return index }
}
return nil
}
}
I was struggling with the tab bar animation both from a user tap and programmatically calling selectedIndex = X since the accepted solution didn't work for me when setting the selected tab programatically.
In the end I managed to solve it by a UITabBarControllerDelegate and a custom UIViewControllerAnimatedTransitioning as follows:
extension MainController: UITabBarControllerDelegate {
public func tabBarController(
_ tabBarController: UITabBarController,
animationControllerForTransitionFrom fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FadePushAnimator()
}
}
Where the FadePushAnimator looks like this:
class FadePushAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let toViewController = transitionContext.viewController(forKey: .to)
else {
return
}
transitionContext.containerView.addSubview(toViewController.view)
toViewController.view.alpha = 0
let duration = self.transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
toViewController.view.alpha = 1
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
This approach supports any sort of custom animation and works both on user tap and setting the selected tab programatically. Tested on Swift 5.
To expand on #gmogames answer: https://stackoverflow.com/a/45362914/1993937
I couldn't get this to animate when selecting the tab bar index via code, as calling:
tabBarController.setSeletedIndex(0)
Doesn't seem to go through the same call heirarchy, and it skips the method:
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
entirely, resulting in no animation.
In my code I wanted to have an animation transition for a user tapping a tab bar item in addition to me setting the tab bar item in-code manually under certain circumstances.
Here is my addition to the solution above which adds a different method to set the selected index via code that will animate the transition:
import Foundation
import UIKit
#objc class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
#objc func set(selectedIndex index : Int) {
_ = self.tabBarController(self, shouldSelect: self.viewControllers![index])
}
}
#objc extension CustomTabBarController: UITabBarControllerDelegate {
#objc func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view, let toView = viewController.view else {
return false // Make sure you want this as false
}
if fromView != toView {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: { (true) in
})
self.selectedViewController = viewController
}
return true
}
}
Now just call
tabBarController.setSelectedWithIndex(1)
for an in-code animated transition!
I still think it is unfortunate that to get this done we have to override a method that isn't a setter and manipulate data within it. It doesn't make the tab bar controller as extensible as it should be if this is the method that we need to override to get this done.
So, a few years later and more experienced, after revisiting my own question for the same behaviour, I improved a little bit upon Derek's answer.
I inherited most of his code (as it seems like the best solution).
What I changed
I added a crossDissolve animation (as I originally wanted) to the slide animation by adding a toCoverView and fromCoverView, these are snapshotviews of the other view which will be used to fade in/out at the same time.
Changed the frame width to already start at 75% instead of having to translate the full 100% width, it's only translating 25% now which makes it feel snappier.
Added SpringWithDamping and initialSpringVelocity settings.
These changes made it feel just about as close as I could get it to Facebook's implementation and I'm personally quite happy with it.
Here's the adapted answer (most of the credit goes to Derek so be sure to upvote him):
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return MyTransition(viewControllers: tabBarController.viewControllers)
}
}
class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {
let viewControllers: [UIViewController]?
let transitionDuration: Double = 0.2
init(viewControllers: [UIViewController]?) {
self.viewControllers = viewControllers
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(transitionDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let fromView = fromVC.view,
let fromIndex = getIndex(forViewController: fromVC),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let toView = toVC.view,
let toIndex = getIndex(forViewController: toVC)
else {
transitionContext.completeTransition(false)
return
}
let frame = transitionContext.initialFrame(for: fromVC)
var fromFrameEnd = frame
var toFrameStart = frame
let quarterFrame = frame.width * 0.25
fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - quarterFrame : frame.origin.x + quarterFrame
toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + quarterFrame : frame.origin.x - quarterFrame
toView.frame = toFrameStart
let toCoverView = fromView.snapshotView(afterScreenUpdates: false)
if let toCoverView = toCoverView {
toView.addSubview(toCoverView)
}
let fromCoverView = toView.snapshotView(afterScreenUpdates: false)
if let fromCoverView = fromCoverView {
fromView.addSubview(fromCoverView)
}
DispatchQueue.main.async {
transitionContext.containerView.addSubview(toView)
UIView.animate(withDuration: self.transitionDuration, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.8, options: [.curveEaseOut], animations: {
fromView.frame = fromFrameEnd
toView.frame = frame
toCoverView?.alpha = 0
fromCoverView?.alpha = 1
}) { (success) in
fromCoverView?.removeFromSuperview()
toCoverView?.removeFromSuperview()
fromView.removeFromSuperview()
transitionContext.completeTransition(success)
}
}
}
func getIndex(forViewController vc: UIViewController) -> Int? {
guard let vcs = self.viewControllers else { return nil }
for (index, thisVC) in vcs.enumerated() {
if thisVC == vc { return index }
}
return nil
}
}
The only thing I've yet to figure out is how to make it "interruptible" like Facebook does. I know there's a interruptibleAnimator function for this but I haven't been able to make it work yet.
I am following tutorial to animate tab bar images. The given line of code doesn't work for me
secondItemView.subviews.first as! UIImageView
as it gets data of type UITabBarButtonLabel and not UIImageView.
I added tag value 1 so that I can get UIImageView using tag value
secondItemView.viewWithTag(1)
returns nil. What is the other way to get UIImageView's reference?
Code
#objc public class MTMainScreenTabsController : UITabBarController {
var secondItemImageView: UIImageView!
public override func viewDidLoad() {
super.viewDidLoad()
let secondItemView = self.tabBar.subviews[0]
print(secondItemView.subviews)
print(secondItemView.viewWithTag(1))
//self.secondItemImageView = secondItemView.subviews.secon
var item: UITabBarItem! = self.tabBar.items![0]
//self.secondItemImageView = secondItemView.viewWithTag(1) as! UIImageView
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
public override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if item.tag == 1 {
self.secondItemImageView.transform = .identity
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
() -> Void in
//let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
let rotation = CGAffineTransform.init(rotationAngle: CGFloat(Double.pi/2))
self.secondItemImageView.transform = rotation
}, completion: nil)
}
}
}
It's not a good practice to strictly depend on the order of subviews as it may change in the future. If you are sure that secondItemView.subviews contain an instance of UIImageView you can find it like this:
let imageView = secondItemView.subviews.flatMap { $0 as? UIImageView }.first
An imageView variable would be an optional containing either nil or an actual UIImageView if there was any subview of this type. Flat map would iterate through subviews and try to cast each to UIImageView - if it fails the result will be filtered out, if not it will then be put in the result array from which you're taking first element.
Use My code this one is help you
class AnimatedTabBarController: UITabBarController {
var secondItemImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let secondItemView = self.tabBar.subviews[1]
self.secondItemImageView = secondItemView.subviews.first as! UIImageView
self.secondItemImageView.contentMode = .center
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if item.tag == 1{
//do our animations
self.secondItemImageView.transform = CGAffineTransform.identity
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: UIViewAnimationOptions(), animations: { () -> Void in
let rotation = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
self.secondItemImageView.transform = rotation
}, completion: nil)
}
}
}