Custom segue transition animation - swift

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
}
}

Related

"unrecognized selector sent to instance" error when adding ViewController

I've recently attempted to add a welcome screen to my AR app that works as a Home screen. When the app loads, the user can tap the button and then the app freezes, crashes and displays the code
"Thread 1: "-[_0_2_2020_2.WelcomeViewController letsGo:]: unrecognized selector sent to instance 0x13ec05e00"
I've tried a few of the solutions available, but I haven't been able to come up with a solution. I think it has something to do with my *IBAction connection. Any assistance is greatly appreciated!
import UIKit
import RealityKit
import ARKit
class WelcomeViewController: UIViewController {
#IBAction func gotPressed(_ sender: Any) {let storyboard = UIStoryboard(name: "Main",
bundle: nil)
if let viewController = storyboard.instantiateViewController(withIdentifier:
"ViewController") as? ViewController {
self.present(viewController, animated: true, completion: nil) /// present the view
controller (the one with the ARKit)!
} }
}
class ViewController: UIViewController, ARSessionDelegate {
//delay app launch to show splash screen
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Thread.sleep(forTimeInterval: 3.0)
// Override point for customization after application launch.
return true
}
//end splash screen delay
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
arView.session.delegate = self
showModel()
overlayCoachingView()
setupARView()
arView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))))
}
func showModel(){
let anchorEntity = AnchorEntity(plane: .horizontal, minimumBounds:[0.7, 0.7])
anchorEntity.scale = [0.2, 0.2, 0.2]
let entity = try! Entity.loadModel(named: "COW_ANIMATIONS")
entity.setParent(anchorEntity)
arView.scene.addAnchor(anchorEntity)
}
//Overlay coaching view "adjust iphone scan"
func overlayCoachingView () {
let coachingView = ARCoachingOverlayView(frame: CGRect(x: 0, y: 0, width: arView.frame.width, height: arView.frame.height))
coachingView.session = arView.session
coachingView.activatesAutomatically = true
coachingView.goal = .horizontalPlane
view.addSubview(coachingView)
}//end overlay
func setupARView(){
arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
configuration.environmentTexturing = .automatic
arView.session.run(configuration)
}
//object placement
#objc
func handleTap(recognizer: UITapGestureRecognizer){
let location = recognizer.location(in:arView)
let results = arView.raycast(from: location, allowing: .estimatedPlane, alignment: .horizontal)
if let firstResult = results.first {
let brownCowAnchor = ARAnchor(name: "COW_ANIMATIONS", transform: firstResult.worldTransform)
arView.session.add(anchor: brownCowAnchor)
} else {
print("Object placement failed - couldn't find surface.")
//cow animations
//let robot = try! ModelEntity.load(named: "COW_ANIMATIONS")
let brownCowAnchor = AnchorEntity()
let blackCowAnchor = AnchorEntity()
//anchor.children.append(robot)
//arView.scene.anchors.append(anchor)
//robot.playAnimation(robot.availableAnimations[0].repeat(duration: .infinity),
//transitionDuration: 0.5,
//startsPaused: false)
//start cow animation
let brownCow = try! ModelEntity.load(named: "COW_ANIMATIONS")
let blackCow = try! ModelEntity.load(named: "Cow")
brownCow.position.x = -1.0
blackCow.position.x = 1.0
brownCowAnchor.position.z = -2.0
blackCowAnchor.position.z = -2.0
brownCow.setParent(brownCowAnchor)
blackCow.setParent(blackCowAnchor)
arView.scene.anchors.append(brownCowAnchor)
arView.scene.anchors.append(blackCowAnchor)
let cowAnimationResource = brownCow.availableAnimations[0]
let horseAnimationResource = blackCow.availableAnimations[0]
brownCow.playAnimation(cowAnimationResource.repeat(duration: .infinity),
transitionDuration: 1.25,
startsPaused: false)
blackCow.playAnimation(horseAnimationResource.repeat(duration: .infinity),
transitionDuration: 0.75,
startsPaused: false)
//end cow animations
}
}
func placeObject(named entityName: String, for anchor: ARAnchor) {
let entity = try! ModelEntity.loadModel(named: entityName)
entity.generateCollisionShapes(recursive: true)
arView.installGestures([.rotation, .translation], for: entity)
let anchorEntity = AnchorEntity(anchor: anchor)
anchorEntity.addChild(entity)
arView.scene.addAnchor(anchorEntity)
}
}
The button is triggering a function letsGo which doesn't appear anywhere on the WelcomeViewController you posted. Check interface builder and make sure that you've removed the old connection from the button. Should be the final tab.

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
//...
}

How can I add a swipe gesture to my side menu controller?

I made a side menu with some controls and I can dismiss it when the user taps outside of the side menu or if he/she selects a row inside the side menu. Now I want to add a swipe gesture to the left so that the user can dismiss it that way too.
extension MenuViewController {
#objc func dismissControllerAnimated() {
dismiss(animated: true, completion: nil)
} }
class SlideinTransition: NSObject, UIViewControllerAnimatedTransitioning {
let menuViewController = MenuViewController()
var isPresenting = true
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width * 0.3
let finalHeight = toViewController.view.bounds.height
if isPresenting{
//adds a tap gesture to our dimming view
let tapGesture = UITapGestureRecognizer(target: toViewController, action: #selector(MenuViewController.dismissControllerAnimated))
dimmingView.addGestureRecognizer(tapGesture)
//adds the dimming view
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.0
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
//adds the menu view controller to our container
containerView.addSubview(toViewController.view)
//init frame off the screen
toViewController.view.frame = CGRect(x: -finalWidth, y: 0, width: finalWidth, height: finalHeight)
}
let transform = {
self.dimmingView.alpha = 0.5
toViewController.view.transform = CGAffineTransform(translationX: finalWidth, y: 0)
}
//applies a specific kind of transformation to our view
let identity = {
self.dimmingView.alpha = 0.0
fromViewController.view.transform = .identity
}
//animates the transition and cancels it when you click outside of the frame
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
if !self.isPresenting {
self.dimmingView.removeFromSuperview()
}
}
}
}
I instantiate my "MenuViewController" like this into my MainController
#IBAction func didTapMenu(_ sender: UIBarButtonItem) {
guard let menuViewController = storyboard?.instantiateViewController(withIdentifier: "MenuViewController") as? MenuViewController else { return }
menuViewController.didTapMenuType = { menuType in
self.transitionToNew(menuType)
}
menuViewController.modalPresentationStyle = .overCurrentContext
menuViewController.transitioningDelegate = self
present(menuViewController, animated: true)
}
How can I add a SwipeGestureRecognizer to my transition? I appreciate every input. Thanks in advance!

Swap rootViewController with animation?

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
}

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.