Swap rootViewController with animation? - swift

Im trying to swap to another root view controller with a tab bar; via app delegate, and I want to add transition animation. By default it would only show the view without any animation.
let tabBar = self.instantiateViewController(storyBoard: "Main", viewControllerID: "MainTabbar")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window = UIWindow(frame: UIScreen.main.bounds)
appDelegate.window?.rootViewController = tabBar
appDelegate.window?.makeKeyAndVisible()
That's how I swapped to another rootview controller.

You can use UIView.transition(with: view) to replace the rootViewController of a UIWindow:
guard let window = UIApplication.shared.keyWindow else {
return
}
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MainTabbar")
// Set the new rootViewController of the window.
// Calling "UIView.transition" below will animate the swap.
window.rootViewController = vc
// A mask of options indicating how you want to perform the animations.
let options: UIView.AnimationOptions = .transitionCrossDissolve
// The duration of the transition animation, measured in seconds.
let duration: TimeInterval = 0.3
// Creates a transition animation.
// Though `animations` is optional, the documentation tells us that it must not be nil. ¯\_(ツ)_/¯
UIView.transition(with: window, duration: duration, options: options, animations: {}, completion:
{ completed in
// maybe do something on completion here
})

Swift 4
Paste function into AppDelegate:
func setRootViewController(_ vc: UIViewController, animated: Bool = true) {
guard animated, let window = self.window else {
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
return
}
window.rootViewController = vc
window.makeKeyAndVisible()
UIView.transition(with: window,
duration: 0.3,
options: .transitionCrossDissolve,
animations: nil,
completion: nil)
}

An alternative solution:
let stb = UIStoryboard(name: "YOUR_STORYBOARD_NAME", bundle: nil)
let rootVC = stb.instantiateViewController(withIdentifier: "YOUR_TABBAR_VIEWCONTROLLER_NAME")
let snapshot = (UIApplication.shared.keyWindow?.snapshotView(afterScreenUpdates: true))!
rootVC.view.addSubview(snapshot)
UIApplication.shared.keyWindow?.rootViewController = rootVC
UIView.transition(with: snapshot,
duration: 0.4,
options: .transitionCrossDissolve,
animations: {
snapshot.layer.opacity = 0
},
completion: { status in
snapshot.removeFromSuperview()
})

Im trying to swap to another root view controller ... and I want to add transition animation
I have an app that does this: it changes the root view controller with animation (it's called Albumen).
But my app actually doesn't actually change the root view controller. The root view controller is a custom container view controller that never changes. Its view is never seen and it has no functionality. Its only job is to be the place where the change happens: it swaps one child view controller for another — and thus the transition animation works.
In other words, you add one view controller to your view controller hierarchy, right at the top of the hierarchy, and the whole problem is solved neatly and correctly.

Try this:
UIView.transition(from: appdelegate.window.rootViewController!.view, to: tabbar.view, duration: 0.6, options: [.transitionCrossDissolve], completion: {
_ in
appdelegate.window.rootViewController = tabbar
})

Updated Swift 5.3 version:
let foregroundedScenes = UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }
let window = foregroundedScenes.map { $0 as? UIWindowScene }.compactMap { $0 }.first?.windows.filter { $0.isKeyWindow }.first
guard let uWindow = window else { return }
uWindow.rootViewController = customTabBarController
UIView.transition(with: uWindow, duration: 0.3, options: [.transitionCrossDissolve], animations: {}, completion: nil)
}

And here is example of transitionCrossDissolve with transform translation Y of snapshotView, I think this looks better than regular transition animation.
Tested with Swift 4~5, iOS 11 ~ 15.7
if let window = UIApplication.shared.keyWindow {
var snapShot = UIView()
let destinationVC = UIViewController()
if let realSnapShot = window.snapshotView(afterScreenUpdates: true) {
snapShot = realSnapShot
}
destinationVC.view.addSubview(snapShot)
window.rootViewController = destinationVC
window.makeKeyAndVisible()
UIView.transition(
with: window,
duration: 0.5,
options: .transitionCrossDissolve,
animations: {
snapShot.transform = CGAffineTransform(translationX: 0, y: snapShot.frame.height)
},
completion: { status in
snapShot.removeFromSuperview()
}
)
}

I have created a helper class for this based on d.felber's answer:
import UIKit
class ViewPresenter {
public static func replaceRootView(for viewController: UIViewController,
duration: TimeInterval = 0.3,
options: UIView.AnimationOptions = .transitionCrossDissolve,
completion: ((Bool) -> Void)? = nil) {
guard let window = UIApplication.shared.keyWindow else {
return
}
guard let rootViewController = window.rootViewController else {
return
}
viewController.view.frame = rootViewController.view.frame
viewController.view.layoutIfNeeded()
UIView.transition(with: window, duration: duration, options: options, animations: {
window.rootViewController = viewController
}, completion: completion)
}
}
You can use it like this:
let loginVC = SignInViewController(nibName: "SignInViewController", bundle: nil)
ViewPresenter.replaceRootView(for: loginVC)
or
ViewPresenter.replaceRootView(for: loginVC, duration: 0.3, options: .transitionCrossDissolve) {
(bool) in
// do something
}

Related

Close MainScreen view controller

I've noticed that when I switch from my main controller view (mainScreen)
to another view (example: secondScreen), I don't close my mainScreen
and open the secondScreen, I just open my secondScreen
on top of my mainScreen. Is there a way for me to close my mainScreen??
The code I use to switch from mainScreen to secondScreen:
let storyboard = UIStoryboard(name: "MainScreen", bundle: nil)
let secondVC = storyboard.instantiateViewController(identifier: "SecondScreen"
self.dismiss(animated: true, completion: nil)
self.loadView() show(secondVC, sender: self)
The code I use to return to mainScreen:
dismiss(animated: true, completion: nil)
// I use self.dismiss() and self.loadView() as a solution to reload MainScreen.
// I take data from SecondScreen that changes data on MainScreen,
// and I can't succeed it with another way.
If we can't have our MainScreen reloaded, at least making a function that could
change my data every time it load my MainScreen view it would be great!
Thank you for your time!
Swift 5 - iOS 14
The following code changes your app root totally.
func coordinateToSecondVC()
{
guard let window = UIApplication.shared.keyWindow else { return }
let storyboard = UIStoryboard(name: "MainScreen", bundle: nil)
let secondVC = storyboard.instantiateViewController(identifier: "SecondScreen")
window.rootViewController = secondVC
window.makeKeyAndVisible()
}
If you want to push your secondVC in navigation stack, you can use this:
func coordinateToSecondVC()
{
guard let window = UIApplication.shared.keyWindow else { return }
let storyboard = UIStoryboard(name: "MainScreen", bundle: nil)
let secondVC = storyboard.instantiateViewController(identifier: "SecondScreen")
let navController = UINavigationController(rootViewController: secondVC)
navController.modalPresentationStyle = .fullScreen
window.rootViewController = navController
window.makeKeyAndVisible()
}
Then easily call the function whenever you want to coordinate to secondVC:
coordinateToSecondVC()

When minimizing a view, root controller contents also minimize swift

Hi there I am trying to recreate apple musics miniplayer controller. There is a view that shows details about the song playing such as the song name, artist name, cover art and so forth like apple music. When a user clicks the dismiss button on the top of that controller, it minimizes it to a view just above the tabBar revealing the rootview behind the view. The only problem is that my code is causing an issue that when the view is minimized, instead of minimizing the view that shows the information about the current song being played, it minimizes all the views and leaves just a black screen. I'm not sure what is causing the issue but I will provide the code for my tabBar controller which houses the code to minimize and maximize the view and then the other controller which calls the function created in the tabBar controller to minimze and maximize the view as well as screen shots of what is happening. Thank you for taking the time to look at this. If anything is unclear please let me know.
TabBarController Code:
import Foundation
import UIKit
import Firebase
class TabBarController: UITabBarController {
var user: User? {
didSet {
guard let nav = viewControllers?[0] as? UINavigationController else { return }
guard let feed = nav.viewControllers.first as? FeedController else { return }
feed.user = user
}
}
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
setupDetailsPlayerView()
// perform(#selector(minimizePlayerDetails), with: nil, afterDelay: 1)
// perform(#selector(maximizePlayerDetails), with: nil, afterDelay: 1)
}
#objc func minimizePlayerDetails() {
maximizeTopAnchorConstraint.isActive = false
minimizeTopAnchorConstraint.isActive = true
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// self.view.layoutIfNeeded()
})
}
#objc func maximizePlayerDetails() {
maximizeTopAnchorConstraint.isActive = true
maximizeTopAnchorConstraint.constant = 0
minimizeTopAnchorConstraint.isActive = false
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
}
func fetchUser() {
guard let uid = Auth.auth().currentUser?.uid else { return }
UserService.shared.fetchUser(uid: uid) { user in
self.user = user
}
}
let playerDetailsView = PlayerDetailController.initFromNib()
var maximizeTopAnchorConstraint: NSLayoutConstraint!
var minimizeTopAnchorConstraint: NSLayoutConstraint!
fileprivate func setupDetailsPlayerView() {
print("setting up details view")
// view.addSubview(playerDetailsView)
view.insertSubview(playerDetailsView, belowSubview: tabBar)
playerDetailsView.translatesAutoresizingMaskIntoConstraints = false
maximizeTopAnchorConstraint = playerDetailsView.topAnchor.constraint(equalTo: view.topAnchor, constant: view.frame.height)
maximizeTopAnchorConstraint.isActive = true
minimizeTopAnchorConstraint = playerDetailsView.topAnchor.constraint(equalTo: tabBar.topAnchor, constant: -64)
// minimizeTopAnchorConstraint.isActive = true
playerDetailsView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
playerDetailsView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
playerDetailsView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
function being called in songcontroller:
#IBAction func dismissTapped(_ sender: Any) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate
else {
return
}
let viewController = TabBarController()
sceneDelegate.window?.rootViewController = viewController
viewController.minimizePlayerDetails()
print("clicked")
self.removeFromSuperview()
}
Screenshots of what is happening:
Normal View:
Minimized View:
If you've created the TabBarController by storyboard then the empty initialization you've provided won't work. You need to instantiate the UIViewController from the storyboard. This seems to be the reason why you're getting an empty UITabBarController when setting the rootViewController. Modify your button action dismissTapped like this:
#IBAction func dismissTapped(_ sender: Any) {
//...
let viewController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "TabBarController") as? TabBarController
sceneDelegate.window?.rootViewController = viewController
//...
}

Moving from SKScene to ViewController

I want to move from my SKScene to a view controller when I touch a SKSpriteNode and back to SKScene when I touch a UIButton. It is working from view controller to SKScene and also the other way around, but only the first time. When I then go back to my scene and the move to the view controller again (the second time), I can hear the music, and it prints everything from viewDidLoad, but it does not the show me UI Elements. I dismissed the scene so that might not be the problem.
Here is the code for moving back from the scene to the view controller.
let currentViewController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
currentViewController.present(ViewController(), animated: false, completion: nil)
or
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier :"VC")
let currentViewController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
currentViewController.present(viewController, animated: false, completion: nil)
Replace the code related to currentViewController with this:
self.view?.window?.rootViewController?.present(viewController, animated: true, completion: nil)
It should work.
You can also create a segue in Storyboard and call it like this:
self.view?.window?.rootViewController?.performSegue(withIdentifier: "VC", sender: self)
EDIT:
I've tried on one of my app now the next solution and it worked:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "VC")
vc.view.frame = rootViewController.view.frame
vc.view.layoutIfNeeded()
UIView.transition(with: window, duration: 0.3, options: .transitionFlipFromRight, animations:
{
window.rootViewController = vc
}, completion: { completed in
// maybe do something here
})
To me it worked, I hope it will solve your case :)

Custom segue transition animation

I'm working on an app that utilizes the navigation controller. I have a certain view controller that has a left and right button in the navigation bar and each button segues to a different view controller. When I press the right button, I just call self.performSegue(withIdentifier: "ToDestination", sender: nil) and when I pop back I call _ = self.navigationController?.popViewController(animated: true).
My issue is when I press the left button. I can push and pop, but I want the transition to work opposite of the right button. Since I'm pressing the left button, I want the segue to transition from left to right, and when I pop, I want the segue to transition from right to left.
I can get the transitions to work the way I want by doing:
//push
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "SearchController") as! SearchController
let transition: CATransition = CATransition()
let timeFunc : CAMediaTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.duration = 0.5
transition.timingFunction = timeFunc
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromLeft
self.navigationController!.view.layer.add(transition, forKey: kCATransition)
self.navigationController!.pushViewController(vc, animated: true)
//pop
let transition: CATransition = CATransition()
let timeFunc : CAMediaTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.duration = 0.5
transition.timingFunction = timeFunc
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
self.navigationController!.view.layer.add(transition, forKey: kCATransition)
_ = self.navigationController?.popViewController(animated: true)
This works, but there is a black flash when transitioning between view controllers. Is there a better way to get the default transition animation?
The best way to achieve that is to create custom push and pop animation by conforming UIViewControllerAnimatedTransitioning protocol:-
//CustomPushAnimation class
import UIKit
class CustomPushAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerVw = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: .from)
let toViewController = transitionContext.viewController(forKey: .to)
guard let fromVc = fromViewController, let toVc = toViewController else { return }
let finalFrame = transitionContext.finalFrame(for: toVc)
//Below line will start from left
toVc.view.frame = finalFrame.offsetBy(dx: -finalFrame.size.width/2, dy: 0)
containerVw.addSubview(toVc.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toVc.view.frame = finalFrame
fromVc.view.alpha = 0.5
}, completion: {(finished) in
transitionContext.completeTransition(finished)
fromVc.view.alpha = 1.0
})
}
}
//CustomPopAnimation class
import UIKit
class CustomPopAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: .from)
let toViewController = transitionContext.viewController(forKey: .to)
guard let fromVc = fromViewController,
let toVc = toViewController,
let snapshotView = fromVc.view.snapshotView(afterScreenUpdates: false)
else { return }
let finalFrame = transitionContext.finalFrame(for: fromVc)
toVc.view.frame = finalFrame
containerView.addSubview(toVc.view)
containerView.sendSubview(toBack: toVc.view)
snapshotView.frame = fromVc.view.frame
containerView.addSubview(snapshotView)
fromVc.view.removeFromSuperview()
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
//Below line will start from right
snapshotView.frame = finalFrame.offsetBy(dx: -finalFrame.size.width, dy: 0)
}, completion: {(finished) in
snapshotView.removeFromSuperview()
transitionContext.completeTransition(finished)
})
}
}
And last you need to present the new View controller with setting
animation flag to true otherwise it will not work
//Below code should be inside YourViewController
let vc = UIStoryboard(name: "storyboardName", bundle: nil).instantiateViewController(withIdentifier: "ViewControllerIdentifier") let navigationController = BaseNavigationController(rootViewController: vc)
navigationController.transitioningDelegate = self
present(navigationController, animated: true, completion: nil)
You also need to conform UIViewControllerTransitioningDelegate
protocol in order to work animation
extension YourViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customPushAnimation
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customPopAnimation
}
}

NSTabViewController - How to create custom transition?

I have an NSTabViewController, and I want to create some custom transition.
I added some NSViewControllerTransitionOptions values and when transition method is called with my values, a custom animation should run.
Bellow is the intermediate code that I written until now. Animation run exactly how what I want, but there is a problem.
nextVC is not presented (I think). That controller should be first responder, after animation that is not respond to keyboard import.
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewControllerTransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
if options.contains(.analogToThemes) {
if let firstVC = fromViewController as? MBCustomizeController {
let nextVC = toViewController
let themesContainer = nextVC.view
themesContainer.setFrameOrigin(NSMakePoint(-250, -510))
var sketchContainer:NSView?
var panelsContainer:NSView?
firstVC.view.addSubview(themesContainer)
for item in firstVC.view.subviews {
if item.identifier == "sketchContainer" {
sketchContainer = item
}
if item.identifier == "customizePanlesContainer"{
panelsContainer = item
}
}
NSAnimationContext.runAnimationGroup({ context in
context.duration = animationDuration
themesContainer.animator().setFrameOrigin(NSMakePoint(0, 0))
sketchContainer!.animator().setFrameOrigin(NSMakePoint(sketchContainer!.frame.origin.x, 520))
panelsContainer!.animator().setFrameOrigin(NSMakePoint(panelsContainer!.frame.origin.x + panelsContainer!.frame.width , 0))
}, completionHandler: {
firstVC.dismiss(nil)
})
}
return
}
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
}
How can I present nextVC correctly?
Thanks.