I am presenting a navigation controller modally, with a table view controller as the root, and pushing more view controllers when the user taps a cell. Now, I want to apply a blur/vibrancy effect to the whole thing.
The storyboard looks like this:
Now, want to apply a blur effect to the whole navigation controller, so that the image in the initial view controller can be seen underneath.
I have somewhat succeeded by applying the same blur effect to both the table view controller and the detail view controller, like this:
Table View Controller
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.tableFooterView = UIView(frame: .zero)
tableView.backgroundColor = UIColor.clear
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
tableView.backgroundView = blurEffectView
tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect)
}
}
Detail View Controller
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: .dark)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(blurView, at: 0)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
}
}
(I also set the modal presentation style of the navigation to .overFullScreen).
However, at the end of the navigation transition to the detail view controller, an annoying artifact can be seen in the blurred background:
I think this has to do with how, since iOS 7, the pushed view controller can not have a transparent background, otherwise the pushing one can be seen as it is instantly removed at the end of the transition.
I tried applying the blur effect to the navigation controller, and make both children's views transparent instead, but it doesn't look good either.
What is the proper way to apply a background blur/vibrancy effect to the contents of a navigation controller, seamlessly?
Try below steps. I haven't tried it but it should work.
Add background Image to window into didFinishLaunchingWithOptions and hide it by default.
Add same image into presenting modal view controller.
Make sure to clear background of your view controller.
So when you present it will show that image which is into modal view controller.
After complition Unhide window image and hide view controller image.
When you tap on back button to dismiss view, first unhide your view controller image and hide your window image. So your animation will look normal.
Let me know if you need any help.
As I figured out in the question, the push/pop animated transitions introduced since iOS 7 (where the view controller that is lower in the navigation stack "slides out at half the speed", so that it underlaps the one being pushed/popped) make it impossible to transition between transparent view controllers without artifacts.
So I decided to adopt the protocol UINavigationControllerDelegate, more precisely the method: navigationController(_:animationControllerFor:from:to:), essentially replicating the animated transitions of UINavigationController from scratch.
This allows me to add a mask view to the view that is transitioned away from during the push operation so that the underlapping section is clipped, and no visible artifacts occur.
So far I have only implemented the push operation (eventually, I'll have to do the pop operation too, and also the interactive transition form swipe gesture-based pop), I have made a custom UINavigationController subclass, and put it on GitHub.
(Currently, it supports animated push and pop, but not interactivity. Also, I haven't figured how to replicate the navigation bar title's "slide" animation yet - it just cross dissolves.)
It boils down to the following steps:
Initially, transform the .to view to the right, off-screen. During the animation, gradually transform it back to identity (center of screen).
Set a white UIView as mask to the .from view, initially with bounds equal to the .from view (no masking). During the animation, gradually reduce the width of the mask's frame property to half its initial value.
During the animation, gradually translate the from view halfway offscreen, to the left.
In code:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
// Super slow for debugging:
let duration: TimeInterval = 1
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from) else {
return
}
guard let toView = transitionContext.view(forKey: .to) else {
return
}
guard let toViewController = transitionContext.viewController(forKey: .to) else {
return
}
//
// (Code below assumes navigation PUSH operation. For POP,
// use similar code but with view roles and direction
// reversed)
//
// Add target view to container:
transitionContext.containerView.addSubview(toView)
// Set tagret view frame, centered on screen
let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
toView.frame = toViewFinalFrame
let containerBounds = transitionContext.containerView.bounds
toView.center = CGPoint(x: containerBounds.midX, y: containerBounds.midY)
// But translate it to fully the RIGHT, for now:
toView.transform = CGAffineTransform(translationX: containerBounds.width, y: 0)
// We will slide the source view out, to the LEFT, by half as much:
let fromTransform = CGAffineTransform(translationX: -0.5*containerBounds.width, y: 0)
// Apply a white UIView as mask to the source view:
let maskView = UIView(frame: CGRect(origin: .zero, size: fromView.frame.size))
maskView.backgroundColor = .white
fromView.mask = maskView
// The mask will shrink to half the width during the animation:
let maskViewNewFrame = CGRect(origin: .zero, size: CGSize(width: 0.5*fromView.frame.width, height: fromView.frame.height))
// Animate:
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {
fromView.transform = fromTransform // off-screen to the left (half)
toView.transform = .identity // Back on screen, fully centered
maskView.frame = maskViewNewFrame // Mask to half width
}, completion: {(completed) in
// Remove mask, or funny things will happen to a
// table view or scroll view if the user
// "rubber-bands":
fromView.mask = nil
transitionContext.completeTransition(completed)
})
}
The result:
(I added a text view to the detail view controller for clarity)
Related
This should be an easy question for most of you. Presenting view controllers like in the attached photo now have a bar at the top of them (see red arrow) to indicate that the user can swipe down to dismiss the controller. Please help with any of the following questions:
What is the proper term for this icon?
Is it part of swift's ui tools / library or is it just a UIImage?
Can someone provide a simple snippet on how to implement - perhaps it is something similar to the code below
let sampleController = SampleController()
sampleController.POSSIBLE_OPTION_TO_SHOW_BAR_ICON = true
present(sampleController, animated: true, completion: nil)
Please see the red arrow for the icon that I am referring to
grabber
grabber is a small horizontal indicator that can appear at the top edge of a
sheet.
In general, include a grabber in a resizable sheet. A grabber shows people that they can drag the sheet to resize it; they can also
tap it to cycle through the detents. In addition to providing a visual
indicator of resizability, a grabber also works with VoiceOver so
people can resize the sheet without seeing the screen. For developer
guidance, see prefersGrabberVisible.
https://developer.apple.com/design/human-interface-guidelines/ios/views/sheets/
From iOS 15+ UISheetPresentationController has property prefersGrabberVisible
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801906-prefersgrabbervisible
A grabber is a visual affordance that indicates that a sheet is
resizable. Showing a grabber may be useful when it isn't apparent that
a sheet can resize or when the sheet can't dismiss interactively.
Set this value to true for the system to draw a grabber in the
standard system-defined location. The system automatically hides the
grabber at appropriate times, like when the sheet is full screen in a
compact-height size class or when another sheet presents on top of it.
Playground snippet for iOS 15:
import UIKit
import PlaygroundSupport
let viewController = UIViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 380, height: 800)
viewController.view.backgroundColor = .white
PlaygroundPage.current.liveView = viewController.view
PlaygroundPage.current.needsIndefiniteExecution = true
let button = UIButton(primaryAction: UIAction { _ in showModal() })
button.setTitle("Show page sheet", for: .normal)
viewController.view.addSubview(button)
button.frame = CGRect(x: 90, y: 100, width: 200, height: 44)
func showModal {
let viewControllerToPresent = UIViewController()
viewControllerToPresent.view.backgroundColor = .blue.withAlphaComponent(0.5)
viewControllerToPresent.modalPresentationStyle = .pageSheet // or .formSheet
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.prefersGrabberVisible = true
}
viewController.present(viewControllerToPresent, animated: true, completion: nil)
}
The feature you are asking is not available in UIKit.
You have to implement custom view-controller transition animation with subclassing UIPresentationController for rendering that pull up/down handle.
UIPresentationController (developer.apple.com)
For custom presentations, you can provide your own presentation controller to give the presented view controller a custom appearance. Presentation controllers manage any custom chrome that is separate from the view controller and its contents. For example, a dimming view placed behind the view controller’s view would be managed by a presentation controller. Apple Documentation
This can be achieved by any UIView or you can use any image if you want by adding subview to UIPresentationController's contentView above the presentedView.
To provide the swipe gesture to dismiss/present, you must implement UIPercentDrivenInteractionController.
You can refer to this tutorial below for detailed understanding.
UIPresentationController Tutorial By raywenderlich.com
You should look for presentationDirection = .bottom in your case.
For gesture driven dismissal, you should check below tutorial
Custom-UIViewcontroller-Transitions-getting-started
I hope this might help you.
If you need to add this indicator within the view controller that is being presented if you do not want to do any custom presentations and just work with the default transitions.
The first thing to think about is your view hierarchy, is the indicator going to be part of your navigation bar or perhaps your view does not have navigation bar - so accordingly you probably need some code to find the correct view to add this indicator to.
In my scenario, I needed a navigation bar so my view controllers were within a navigation controller but you could do the same inside your view controllers directly:
1: Subclass a Navigation Controller
This is optional but it would be nice to abstract away all of this customization into the navigation controller.
I do a check to see if the NavigationController is being presented. This might not be the best way to check but since this is not part of the question, refer to these answers to check if a view controller was presented modally or not
class CustomNavigationController: UINavigationController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this checks if the ViewController is being presented
if presentingViewController != nil {
addModalIndicator()
}
}
private func addModalIndicator() {
let indicator = UIView()
indicator.backgroundColor = .tertiaryLabel
let indicatorSize = CGSize(width: 30, height: 5)
let indicatorX = (navigationBar.frame.width - indicatorSize.width) / CGFloat(2)
indicator.frame = CGRect(origin: CGPoint(x: indicatorX, y: 8), size: indicatorSize)
indicator.layer.cornerRadius = indicatorSize.height / CGFloat(2.0)
navigationBar.addSubview(indicator)
}
}
2: Present the Custom Navigation Controller
let someVC = UIViewController()
let customNavigationController = CustomNavigationController()
customNavigationController.setViewControllers([stationsVC], animated: false)
present(playerNavigationController, animated: true) { }
3: This will produce the following results
You might need to alter some logic here based on your scenario / view controller hierarchy but hopefully this gives you some ideas.
I have seen a lot of tutorial but did not see any interesting me. I want to make custom segue that moves only one view, not the whole page.
Something like this:
Top view is just poping without any animation. But bottom view normally slides on bottom viewcontroler
I have tried:
swift
override func perform() {
scale()
}
func scale(){
let toViewController = self.destination
let fromViewController = self.source
let containerView = fromViewController.view.superview
let originalCenter = fromViewController.view.center
toViewController.view.viewWithTag(1)?.viewWithTag(2)?.transform = CGAffineTransform(translationX:self.source.view.frame.width,y:0)
toViewController.view.center = originalCenter
containerView?.addSubview(toViewController.view)
UIView.animate(withDuration: 0.5, delay:0, options: .showHideTransitionViews ,animations:{
toViewController.view.viewWithTag(1)?.viewWithTag(2)?.transform = CGAffineTransform.identity
},completion:{success in fromViewController.present(toViewController,animated: false,completion: nil)})
}
If you do not understand, ask me and help me please.
I think you need this hierarchy
MainView
-topView
-containerView
-navigation
-firstvc
-secondvc
and set navigationBar to hidden in storyboard
I'm using a custom segue in order to transition between two view controllers. This is the segue I'm using for one of those transitions:
class SegueFromRight: 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(0.0, screenHeight, screenWidth, 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.4, animations: { () -> Void in
src?.frame = (src?.frame.offsetBy(dx: -screenWidth, dy: 0.0))!
dst?.frame = (dst?.frame.offsetBy(dx: -screenWidth, dy: 0.0))!
}) { (Finished) -> Void in
self.source.present(self.destination as UIViewController,
animated: false,
completion: nil)
}
}
}
The segue works perfectly, however the animation is not how I'd like it. When it "pushes" the destination view controller, it's shown black. It only turns to the actual view controller as soon as the animation is finished. I guess that's because the new view controller is only loaded once the animation is finished. But how would I go about preloading the new view controller, so that the animation looks smooth?
The unwinding segue does not display that black animation, as the destination view controller (the previous source view controller) is already loaded of course.
This is likely your problem dst?.frame = CGRect(0.0, screenHeight, screenWidth, screenHeight).
Try this instead dst?.frame = CGRect(screenWidth, 0.0, screenWidth, screenHeight).
Your original line sets the destination view to be off the bottom of the screen rather than off to the right edge of the screen. When you animate the views by the screen width the destination slides from directly below the screen to below and to the left.
I've a custom transition between two controller that works close to the iOS mail app, which one stays on top of the other with some implemented scrolling behavior.
If I present a new view controller from the Presented view controller which isn't full screen sized, and then I dismiss this new presented view controller, the previous Presented view controller changes its height and then resizes itself.
I know this might be a little confusing but check the gif example below.
As you can see, If I present this custom image picker and then dismiss it, the view controller which presented it warps to full screen and then resizes to the initial value.
How can I prevent this from happening? I want the ViewController which presents the image picker keeps its height.
After the dismiss you can see the resize happening.
Setting the presenting view controllers size
Since it's a UIViewControllerAnimatedTransitioning I create a custom presentation and the size it's set has it's own identity
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
let containerBounds = self.containerView?.bounds
let origin = CGPoint(x: 0.0, y: ((containerBounds?.size.height)! * 0.05))
let size = CGSize(width: (containerBounds?.size.width)! , height: ((containerBounds?.size.height)! * 0.95))
// Applies the attributes
let presentedViewFrame: CGRect = CGRect(origin: origin, size: size)
return presentedViewFrame
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
Any hint?
thanks
I think that is where the issue is. You are forcing the frame size which is not working out. You should use something like preferredContentSize.
You can simply add this to the viewDidLoad of your CustomPresentationController.
Alternatively you may also try modalPresentationStyle as "Over Current Context"
You can refer very good examples of how you can keep some part of VC as transparent here
i have to push a viewController that has a dimming view with an alpha of 0.5. Because of this, the ViewController's view has to show the previous controller's view behind this dimming background. The problem is that i'm using a navigationController that uses a UIViewControllerAnimatedTransitioning protocol to customize the animation. By default, after pushing the new viewController onto the stack, the navigationController automatically removes the previous view. So, how to keep the previous view after completing this transition, is this possible?
NOTES: i don't want to just add the controller's view to the navigationController(This gave me strange behaviors in the navigation functionality), and i do really need to push it in this way, so i can continue using the application code pattern.
CODE:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if isPresenting {
let ContainerView = transitionContext.containerView()
if let PresentedController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) {
if let PresentedView = transitionContext.viewForKey(UITransitionContextToViewKey) {
PresentedView.alpha = 0
PresentedView.frame = transitionContext.finalFrameForViewController(PresentedController)
ContainerView.addSubview(PresentedView)
// i've also tried to add the fromView in the containerView.
UIView.animateWithDuration(0.4, animations: {
PresentedView.alpha = 1
}) {
Completion in
transitionContext.completeTransition(Completion)
}
}
}
} else {
// dismiss code...
}
}
Thanks for your patience.
I had a similar situation a few weeks ago. In my case, I was presenting my view controller modally. The way I got around the problem was to take a snapshot of the fromViewController's view and use it as background for toViewController's view.
To capture snapshot:
UIImage *snapshotImage = nil;
UIGraphicsBeginImageContextWithOptions(self.tabBarController.view.frame.size, NO, 0.0);
if ([self.tabBarController.view drawViewHierarchyInRect:self.tabBarController.view.bounds afterScreenUpdates:YES]) {
snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
You can capture transitionContext.containerView for this. Pass this image to your new VC and set it as the background to that VC's view as:
self.view.backgroundColor = [UIColor colorWithPatternImage:self.bgImage];
P.S. I am not fluent in Swift so you will have to convert the code to swift yourself but hopefully this gives you a decent starting point