I am making an app that walks the user through account creation in a series of steps. After each step is completed, the user is taken to the next view controller and a progress bar animates across the top of the screen to communicate how much of the account making process has been completed. Here is the end result:
This is accomplished by placing a navigation controller within a containing view. The progress bar is laid over the containing view and every time the navigation controller pushes a new view controller, it tells the containing view controller to animate the progress bar to a certain percentage of the superview's width. This is done through the following updateProgressBar function.
import UIKit
class ContainerVC: UIViewController {
var progressBar: CAShapeLayer!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
progressBar = CAShapeLayer()
progressBar.bounds = CGRect(x: 0, y: 0, width: 0, height: view.safeAreaInsets.top)
progressBar.position = CGPoint(x: 0, y: 0)
progressBar.anchorPoint = CGPoint(x: 0, y: 0)
progressBar.backgroundColor = UIColor.secondaryColor.cgColor
view.layer.addSublayer(progressBar)
}
func updateProgressBar(to percentAsDecimal: CGFloat!) {
let newWidth = view.bounds.width * percentAsDecimal
CATransaction.begin()
CATransaction.setCompletionBlock({
self.progressBar.bounds.size.width = newWidth
})
CATransaction.commit()
let anim = CABasicAnimation(keyPath: "bounds")
anim.isRemovedOnCompletion = true
anim.duration = 0.25
anim.fromValue = NSValue(cgRect: CGRect(x: 0, y: 0, width: progressBar.bounds.width, height: view.safeAreaInsets.top))
anim.toValue = NSValue(cgRect: CGRect(x: 0, y: 0, width: newWidth, height: view.safeAreaInsets.top))
progressBar.add(anim, forKey: "anim")
}
}
The view controllers in the navigation controller's stack will call this updateProgressBar function when pushing the next VC. This is done like so:
class FourthViewController: UIViewController {
var containerVC: ContainerViewController!
...
#IBAction func nextButtonPressed(_ sender: Any) {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let fifthVC = storyboard.instantiateViewController(withIdentifier: "FifthVC") as! FifthViewController
fifthVC.containerVC = containerVC
navigationController!.pushViewController(fifthVC, animated: true)
//We pass 5/11 because the next step will be step 5 out of 11 total steps
self.containerVC.updateProgressBar(to: 5/11)
}
}
Similarly, when pressing the back button, we shrink the container VC's progress bar:
class FourthViewController: UIViewController {
var containerVC: ContainerViewController!
...
#IBAction func backButtonPressed(_ sender: Any) {
navigationController!.popViewController(animated: true)
//We pass 3/11 because the previous step is step 3 out of 11 total steps
containerVC.updateProgressBar(to: 3/11)
}
}
My problem is that this animation only sometimes works. The progress bar always works when moving forward in the process, but sometimes, when a user navigates back, the bar gets stuck and will no longer move in either direction until an unreached view controller is presented. See the video below:
Video of Bug (Bug begins around 0:23)
I have confirmed that the presentation of an Alert Controller is not the cause of the failure to animate, and have also made sure that the animation is occurring on the main thread. Any suggestions?
As very well explained in this answer here, viewDidLayoutSubviews() gets called more than once.
In your case, you're ending up instatiating a new CAShapeLayer every time you push or pop a view controller from the navigation stack.
Try using viewDidAppear() instead.
Related
basically my current setup is like this
one storyboard ViewController with 3 types of UI View(container, front view, back view) inside of it.
what i want to accomplish (and i don't know how to implement #2)
user enters the data on the form(front of the card- View Controller number 1)
clicks the save button (do animation flipping and redirect to a new view controller)
the new view controller loads up (back of the card - View Controller number 2)
this is the current code flip example:
import UIKit
class HomeViewController: UIViewController {
#IBOutlet weak var goButton: UIButton!
#IBOutlet weak var optionsSegment: UISegmentedControl!
let owlImageView = UIImageView(image: UIImage(named:"img-owl"))
let catImageView = UIImageView(image: UIImage(named:"img-cat"))
var isReverseNeeded = false
override func viewDidLoad() {
super.viewDidLoad()
title = "Transitions Test"
setupView()
}
fileprivate func setupView() {
let screen = UIScreen.main.bounds
goButton.layer.cornerRadius = 22
//container to hold the two UI views
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
containerView.backgroundColor = UIColor(red: 6/255, green: 111/255, blue: 165/255, alpha: 1.0)
containerView.layer.borderColor = UIColor.white.cgColor
containerView.layer.borderWidth = 2
containerView.layer.cornerRadius = 20
containerView.center = CGPoint(x: screen.midX, y: screen.midY)
view.addSubview(containerView)
//front view
catImageView.frame.size = CGSize(width: 100, height: 100)
catImageView.center = CGPoint(x: containerView.frame.width/2, y: containerView.frame.height/2)
catImageView.layer.cornerRadius = 50
catImageView.clipsToBounds = true
//back view
owlImageView.frame.size = CGSize(width: 100, height: 100)
owlImageView.center = CGPoint(x: containerView.frame.width/2, y: containerView.frame.height/2)
owlImageView.layer.cornerRadius = 50
owlImageView.clipsToBounds = true
containerView.addSubview(owlImageView)
}
#IBAction func goButtonClickHandler(_ sender: Any) {
doTransition()
}
fileprivate func doTransition() {
let duration = 0.5
var option:UIViewAnimationOptions = .transitionCrossDissolve
switch optionsSegment.selectedSegmentIndex {
case 0: option = .transitionFlipFromLeft
case 1: option = .transitionFlipFromRight
case 2: option = .transitionCurlUp
case 3: option = .transitionCurlDown
case 4: option = .transitionCrossDissolve
case 5: option = .transitionFlipFromTop
case 6: option = .transitionFlipFromBottom
default:break
}
if isReverseNeeded {
UIView.transition(from: catImageView, to: owlImageView, duration: duration, options: option, completion: nil)
} else {
UIView.transition(from: owlImageView, to: catImageView, duration: duration, options: option, completion: nil)
}
isReverseNeeded = !isReverseNeeded
}
}
There are a few alternatives for transition between view controllers with a flipping animation:
You can define a segue in IB, configure that segue to do a horizontal flipping animation:
If you want to invoke that segue programmatically, give the segue a “Identifier” string in the attributes inspector and then you can perform it like so:
performSegue(withIdentifier: "SecondViewController", sender: self)
Alternatively, give the actual destination view controller’s scene a storyboard identifier, and the presenting view controller can just present the second view controller:
guard let vc = storyboard?.instantiateViewController(identifier: "SecondViewController") else { return }
vc.modalTransitionStyle = .flipHorizontal
vc.modalPresentationStyle = .currentContext
show(vc, sender: self)
If this standard flipping animation isn’t quite what you want, you can customize it to your heart’s content. iOS gives us rich control over custom transitions between view controller by specifying transitioning delegate, supplying an animation controller, etc. It’s a little complicated, but it’s outlined in WWDC 2017 Advances in UIKit Animations and Transitions: Custom View Controller Transitions (about 23:06 into the video) and WWDC 2013 Custom Transitions Using View Controllers.
I have seen variations of the following code all over StackOverflow:
import UIKit
class segueFromLeft: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
let src = self.source.view as UIView!
let dst = self.destination.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
// Specify the initial position of the destination view.
dst?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth,
height: screenHeight)
// Access the app's key window and insert the destination view
above the current (source) one.
let window = UIApplication.shared.keyWindow
window?.insertSubview(dst!, aboveSubview: src!)
// Animate the transition.
UIView.animate(withDuration: 0.5, animations: { () -> Void in
src?.frame = (src?.frame.offsetBy(dx: -screenWidth, dy: 0))!
dst?.frame = (dst?.frame.offsetBy(dx: -screenWidth, dy: 0))!
}) { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: nil) {
}
}
}
}
At first, the code operates as a nice way of transitioning from one view to another. But with continued use, most of the problems that have been listed on this website as a result from it relate to memory. Every time the segue is used, the destination view is initialized and the source view remains in memory. With continued use, the memory use continues to grow and grow.
A simple dismissal of the source view did not function for me, the screen just went black.
My question is, how can we fix this problem?
I have a view controller (OrangeVC) that I add to a class that contains a new keyWindow(NewKeyWindowClass). A button in a different vc is tapped and it triggers this new window to get shown over the app's main window and it animates from the right side bottom of the screen to fill to the top. The animation works fine, it starts from the bottom and fills the screen with a new vc with a orange background. The problem is once the OrangeVC is added to the NewKeyWindowClass the orangeVC's deinit keeps getting triggered.
Why is it's deinit running?
Class that goes inside Animator Class:
class OrangeController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
deinit {
print("OrangeVC -Deinit")
}
}
AnimatorClass:
import UIKit
class NewKeyWindowClass: NSObject {
func animateOrangeVCFromBottomToTop() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
let orangeVC = OrangeController()
// 1. starting frame
orangeVC.view.frame = CGRect(x: keyWindow.frame.width - 10, y: keyWindow.frame.height - 10, width: 10, height: 10)
keyWindow.addSubview(orangeVC.view)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// 2. ending frame
orangeVC.view.frame = keyWindow.frame
})
}
}
Button from a different class that triggers the animation:
#IBAction func triggerAnimationButtonPressed(_ sender: UIButton) {
let newKeyWindowClass = NewKeyWindowClass()
newKeyWindowClass.animateOrangeVCFromBottomToTop()
}
I got the answer from this reddit
An iOS application must have a rootViewController, create one and set
the keyWindow.rootViewController property to it. Then present your
view controller from that. Or just the rootViewController to be your
View Controller actually.
The reason the RedVC kept running it's deinit was because the keyWindow didn't have a rootViewController. I added the RedVC's view as a subview to the keyWindow keyWindow.addSubview(orangeVC.view) instead of making it it's rootVC:
keyWindow.rootViewController = redVC
Once I added it that the RedVC's deinit no longer ran when the animation occurred.
It should be noted that although it stopped the deinit from running I lost the animation and it also made the original keyWindow disappear. I should actually add this to a different UIWindow.
I would like to present a UIViewController on top of the current view controller and set it's height to ~80% of the screen size. I've got the first part:
let additionalVC = ChartsViewController(currentSelection)
additionalVC = .overCurrentContext
present(additionalVC, animated: true)
I tried setting the self.view.frame inside my ChartsVC in viewDidLoad and couple of different things but it is always presented in the full screen mode.
That's what I want to achieve:
blueVC - currentVC
redVC - ChartsVC - VC on top of the current VC with ~80% of the original height
btw I'm doing everything programmatically, no xib and UIStoryboard.
There's a number of ways to achieve this.
You could use a 3rd party framework (http://transitiontreasury.com/) or the way I would do this.
Present the newVC where a transition = model over current context
ensure the newVC.views background color is clear
add another view where origin.y is the distance between the top and the desired gap. This is the view where all your objects will sit on.
If you need a coding example let me know, but its a pretty simple solution and looking at your code your 80% there.
Thomas
Implement a custom UIPresentationController. To use a custom view size, you only need to override a single property.
This code will simply inset the presented view controller by 50x100 pts:
class MyPresentationController: UIPresentationController {
// Inset by 50 x 100
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
}
To darken the presenting view controller, override presentationTransitionWillBegin() and dismissalTransitionWillBegin() to insert a shading view and animate it into view:
class MyPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
let shadeView = UIView()
override func presentationTransitionWillBegin() {
self.shadeView.backgroundColor = UIColor.black
self.shadeView.alpha = 0
// Insert the shade view above the presenting view controller
self.shadeView.frame = self.presentingViewController.view.frame
self.containerView?.insertSubview(shadeView,
aboveSubview: self.presentingViewController.view)
// Animate it into view
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.3
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.0
}, completion: nil)
}
}
To use your custom presentation controller, set the modalPresentationStyle and transitioningDelegate:
class MyViewController : UIViewController, UIViewControllerTransitioningDelegate {
//
// Your code
//
func presentCharts() {
let additionalVC = ChartsViewController(currentSelection)
additionalVC.modalPresentationStyle = .custom
additionalVC.transitioningDelegate = self
self.present(additionalVC, animated: true)
}
//
// UIViewControllerTransitioningDelegate protocol
//
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
return MyPresentationController(presentedViewController: presented,
presenting: presenting)
}
}
In IOS 13 and Xcode 11, you can present ViewController with modalPresentationStyle = .automatic
Take Two ViewController.First view controller have a button and the button action name is clicked.The target is to clicking the button we want to add secondVC as a child of first view controller and show secondVC 80% of the first view controller.again click button we remove secondVC from first view controller. below is the code for click button action.
#IBAction func clicked(_ sender: UIButton) {
if !isshown{
isshown = true
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
let height = view.frame.height
let width = view.frame.width
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)
}, completion: { (result) in
// do what you want to do
})
}else{
isshown = false
UIView.animate(withDuration: 0.3,
delay: 0,
options: UIViewAnimationOptions.curveEaseIn,
animations: { () -> Void in
var frame = self.vc.view.frame
frame.origin.y = UIScreen.main.bounds.maxY
self.vc.view.frame = frame
}, completion: { (finished) -> Void in
self.vc.view.removeFromSuperview()
self.vc.removeFromParentViewController()
})
}
}
here vc is a reference of secondVC.
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Second") as! secondVC
change below piece of code to get whatever percentage you want.
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)
If our app has a network connectivity error we would like to overlay a colored and transparent rectangle over the top of the screen with some "Network not available" like text. The rectangle should cover the full width of the screen and the height should be enough to just show the text. We would use a timer to only show the rectangle for a brief period of time. How can you do this?
The actual view may be a UITableViewController, a UIViewController, or something else...
You can do this:
let deadlineTime = DispatchTime.now() + .seconds(2)
let window = UIApplication.shared.keyWindow!
let rectangleView = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 20))
rectangleView.backgroundColor = UIColor.red
let label = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 20))
label.text = "Network not available"
rectangleView.addSubview(label)
window.addSubview(rectangleView)
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
rectangleView.removeFromSuperview()
}
The way I have done overlays is to drag a new UIViewController into a storyboard, and drag a UIView into that. While laying out the UI it can be helpful to make the background color of the UIViewController black. When you're done laying out your elements inside the UIView, change the background color of the UIViewController to transparent.
Here's an example of a profile overlay:
In this case I've actually made the UIViewController background a gray color with an alpha of about 50%. Then when I present this view controller using a fade transition it looks like it appears over top the current context:
func showOverlay() {
//
guard let vc = UIStoryboard(name: "MyStoryboard", bundle: nil).instantiateViewController(withIdentifier: "myOverlay") as? UIViewController else {
print("failed to get myOverlay from MyStoryboard")
return
}
vc.modalPresentationStyle = .overCurrentContext
vc.modalTransitionStyle = .crossDissolve
self.present(vc, animated: true, completion: {
// after 3 seconds, dismiss the overlay
dispatchAfterSeconds(3) {
vc.dismiss(animated: true)
}
})
}
This uses a handy function, dispatchAfterSeconds:
// execute function after delay using GCD
func dispatchAfterSeconds(_ seconds: Double, completion: #escaping (() -> Void)) {
let triggerTime = Int64(Double(NSEC_PER_SEC) * seconds)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(triggerTime) / Double(NSEC_PER_SEC), execute: { () -> Void in
completion()
})
}
Note that when I talk about changing the background color of the UIViewController, what I actually mean by that is the background color of the view created by default inside of a UIViewController that has been created in a storyboard.