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.
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 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.
I'm using a custom UIPresentationController to present a view modally. After presenting the view, the first textfield in the presented view becomes the first responder and the keyboard shows up. To ensure that the view is still visible, I move it up. However, when I do this the frameOfPresentedViewInContainerView is not matching the actual frame of the view anymore. Because of this, when I tap on the view it's being dismissed, because there's a tapGestureRecogziner on the backgroundView which is on top of the presentingView. How to notify the presentingController that the frame/position of the presentedView has changed?
In the UIPresentationController:
override var frameOfPresentedViewInContainerView: CGRect {
var frame = CGRect.zero
let safeAreaBottom = self.presentingViewController.view.safeAreaInsets.bottom
guard let height = presentedView?.frame.height else { return frame }
if let containerBounds = containerView?.bounds {
frame = CGRect(x: 0,
y: containerBounds.height - height - safeAreaBottom,
width: containerBounds.width,
height: height + safeAreaBottom)
}
return frame
}
override func presentationTransitionWillBegin() {
if let containerView = self.containerView, let coordinator = presentingViewController.transitionCoordinator {
containerView.addSubview(self.dimmedBackgroundView)
self.dimmedBackgroundView.backgroundColor = .black
self.dimmedBackgroundView.frame = containerView.bounds
self.dimmedBackgroundView.alpha = 0
coordinator.animate(alongsideTransition: { _ in
self.dimmedBackgroundView.alpha = 0.5
}, completion: nil)
}
}
Presenting the view modally:
let overlayVC = CreateEventViewController()
overlayVC.transitioningDelegate = self.transitioningDelegate
overlayVC.modalPresentationStyle = .custom
self.present(overlayVC, animated: true, completion: nil)
Animation when keyboard appears (in the presented view):
#objc func animateWithKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
guard let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
return
}
// bottomContraint is the constraint that pins content to the bottom of the superview.
let moveUp = (notification.name == UIResponder.keyboardWillShowNotification)
bottomConstraint.constant = moveUp ? (keyboardHeight) : originalBottomValue
let options = UIView.AnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
From the Apple documentation:
UIKit calls this method multiple times during the course of a
presentation, so your implementation should return the same frame
rectangle each time. Do not use this method to make changes to your
view hierarchy or perform other one-time tasks.
AFAIK, if you specify frame through this variable, it's advised not to change it throughout the course of presentation. If you plan to play around with the frames, don't specify this variable and handle all the changes manually in your animator
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)