custom segue that moves only part of content xcode - ios

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

Related

Modal Navigation Controller with Blurred Background, Seamless During Push Animation?

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)

Navigation Bar is Black during Custom Unwind Segue

I made a custom UIStoryboardSegue but when it unwinds, it seems to cause my UINavigationBar to go black, then back to it's correct color. See the GIF below.
My custom segue just makes the the new ViewController come down from the top and leave going back up to the top.
Here is the UIStoryboardSegue code:
import UIKit
class SlideDownSegue: UIStoryboardSegue {
var duration: Double = 0.5
override func perform() {
let screenHeight = UIScreen.main.bounds.size.height
let toVC = self.destination
let fromVC = self.source
toVC.view.transform = CGAffineTransform(translationX: 0, y: -screenHeight)
UIApplication.shared.keyWindow?.insertSubview(toVC.view, aboveSubview: fromVC.view)
UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
toVC.view.transform = CGAffineTransform.identity
}, completion: {
success in
fromVC.present(toVC, animated: false, completion: nil)
})
}
}
class UnwindSlideDownSegue: UIStoryboardSegue {
override func perform() {
let screenHeight = UIScreen.main.bounds.size.height
let toVC = self.destination
let fromVC = self.source.parent!
fromVC.view.superview?.insertSubview(toVC.view, at: 0)
UIView.animate(withDuration: 0.5, delay: 0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
fromVC.view.transform = CGAffineTransform(translationX: 0, y: -screenHeight - 100)
}, completion: {
success in
fromVC.dismiss(animated: false, completion: nil)
})
}
}
If I let the unwind do the default where it just leaves by going to to the bottom of the screen but keep my custom for showing the new View, the UINavigationBar maintains it's correct color, it's only when I used my code provided for unwind that the UINavigationBar goes black during the animation.
Any hints would be much appreciated.
---EDIT---
I played with it a little, if I go into the AppDelegate and change UINavigationBar.appearance().isTranslucent = false to true, I instead get a white background, but then it just appears that the Navigation Bar is suddenly appearing. I'm wondering if it is for some reason being unloaded and then loaded back in once the View Controller is active.
---EDIT 2---
I am sorta able to fix it with a hack. In the AppDelegate inside func application(... didFinishLaunchingWithOptions ...) I added in self.window?.backgroundColor = UIColor.{your color} but all that does is make that black part now appear my color, the Navigation Bar is still disappearing during the segue for some reason.
This is a perfect opportunity to use snapshotView(afterScreenUpdates:). That's how UIKit performs many of their stock transitions (even in UINavigationController) and it's a tremendous way to perform custom transitions, particularly if you want to apply effects. Take a snapshot of the destination (which will have an intact navigation bar), insert it right below the source, perform the animation, dismiss the source so that the destination is ready, and finally remove the snapshot which you won't even notice because it will mirror the destination exactly.
If you want a simple trick ,try to set background color to window in AppDelegate's didFinishLaunchingWithOptions to your navigationbar's color, it works for me :)
like this
window?.backgroundColor = myColor

Custom animation of segue is not played the first time tapping the button

I encountered an unusual behavior on which I am stuck a little, the problem is the following.
I'm using BWWalkthrough library in order to have a 4 slides as launch screen. So in my appdelegate i have the following code which initialize the viewcontrollers:
let storyboard = UIStoryboard(name: "SlidesFlow", bundle: nil)
let walkthrough = storyboard.instantiateViewController(withIdentifier: "SlidesView") as! BWWalkthroughViewController
let page_zero = storyboard.instantiateViewController(withIdentifier: "page_1")
let page_one = storyboard.instantiateViewController(withIdentifier: "page_2")
let page_two = storyboard.instantiateViewController(withIdentifier: "page_3")
let page_three = storyboard.instantiateViewController(withIdentifier: "page_4")
walkthrough.delegate = self
walkthrough.addViewController(page_zero)
walkthrough.addViewController(page_one)
walkthrough.addViewController(page_two)
walkthrough.addViewController(page_three)
Everything works as intended, so no problem here. On the viewController page_three i have a button which redirect me to an other view controller using a custom segue animation
class sentSegueFromRight: UIStoryboardSegue {
override func perform()
{
let src = self.source as UIViewController
let dst = self.destination as UIViewController
src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
dst.view.transform = CGAffineTransform(translationX: src.view.frame.size.width, y: 0)
UIView.animate(withDuration: 0.25,
delay: 0.0,
options: UIViewAnimationOptions.curveEaseInOut,
animations: {
dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
},
completion: { finished in
src.present(dst, animated: false, completion: nil)
}
)
}
}
Now the problem is, if i use the same code on a normal viewcontroller the button and the animation work without issues. The problem is when i use the segue defined above from the last slide of my BWWalkthrough. The first time i tapp the button the viewcontroller which should appear does appear but without the corresponding animation. After closing it and taping on the button again the animation is played but an error is returned:
Presenting view controllers on detached view controllers is
discouraged
If i use the button with the standard animation ( without using my custom animation code ) i get no error and the default animation is played.
I can't seem to find a solution to this problem. Does anybody stumbled upon something like this?
The problem here lies in the BWWalkthrough library which is using a scrollview to present all the views of the various ViewControllers that you add.
As such, you add the dst.view at the beginning of the scrollview (at offset screenwidth,0) which you then transform to offset (0,0).
All of this is offscreen as you are currently in the third screen of the walkthrough (at offset (screenwidth*3,0)). As such you don't get to see the animation and directly see the presented view controller when the segue ends.
To remedy this, add your dst.view in the segue to the superview of the scrollview. i.e. instead of src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
write src.view.superview?.superview?.insertSubview(dst.view, aboveSubview: src.view) in the segue. (Assuming that you are using the segue only from the walkthrough)
If you intend to use the segue in other places too, then you can maybe add a type check in the segue to check if the superview of src.view is a scrollview, if yes, add dst.view to the superview of the scrollview.

segueForUnwindingToViewController causing "Warning: Attempt to present <...> on <...> which is already presenting <...>"

I am building an app and recently discovered a huge memory leak caused by traditional segues.
Therefore I learned about unwind segue. Everything works just fine if I simply use:
#IBAction func prepareForUnwindToMainFromFriends(segue: UIStoryboardSegue) {
}
Memory leak is solved and 'everything is awesome'. But this solution looks ugly on a UI point of view. So I implemented this function from this website. And changed it a little.
override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController) {
let fromView = fromViewController.view
let toView = toViewController.view
if let containerView = fromView.superview {
let initialFrame = fromView.frame
var offscreenRect = initialFrame
var offscreenRectFinal = initialFrame
offscreenRect.origin.x += CGRectGetWidth(initialFrame)
offscreenRectFinal.origin.x -= CGRectGetWidth(initialFrame)
toView.frame = offscreenRect
containerView.addSubview(toView)
let duration: NSTimeInterval = 1.0
let delay: NSTimeInterval = 0.0
let options = UIViewAnimationOptions.CurveEaseInOut
let damping: CGFloat = 0.9
let velocity: CGFloat = 4.0
UIView.animateWithDuration(duration, delay: delay, usingSpringWithDamping: damping,
initialSpringVelocity: velocity, options: options, animations: {
toView.frame = initialFrame
fromView.frame = offscreenRectFinal
}, completion: { finished in
fromView.removeFromSuperview()
if let navController = toViewController.navigationController {
navController.popToViewController(toViewController, animated: false)
}
})
}
}
}
But now I get an error message:
2015-05-12 08:47:31.841 PING0.4[4343:1308313] Warning: Attempt to present <NotificationViewController: 0x177030b0> on <PING0_4.ViewController: 0x16271000> which is already presenting <NotificationViewController: 0x1a488170>
And I am blocked in my app. I can go from VC1 to VC2, then back to VC2 but then I cannot get back to VC1 again. It looks like I can only use this segue once.
Any one has any idea of what is going on?
Created Sample code for unwind segue with above transition animation code. Checkout SampleUnwind project that will help you to understand unwind segue(and how simple it is).
In project there is one navigation controller and inside it there are three view controller (Home->First->second).
In Home controller following unwind action is created, which will be called when 'Home' button of second controller is tapped(simple unwind stuff).
#IBAction func unwindToHomeViewController(segue:UIStoryboardSegue) {
}
I have created TempNavigationController subclassing UINavigationController and set that TempNavigationController to navigation controller in storyboard.
Above method you given is pesent inside it as this will be container of fromViewController as per following referance.
Reference: Apple documentation about Transitioning Between Two Child View Controllers.
You can compare this with your project and maybe you can find any duplicate(or multiple/wrong) segue in your project.

View transition animation having a snapshot

I have several swipe and pan gestures to navigate through some views. When I pan the view, a new ViewController will be instantiated and placed at the edge of the window. Along my pan, the view will come into view on top of the current view. If the pan has passed halfway, the new view will automatically finish and replaces the current view and on completion removing the old view.
var newViewController: UIViewController! {
didSet {
if let newView = NewViewController {
addChildViewController(newView)
newView.view.frame.origin.x = view.bounds.width }
view.addSubview(newView.view)
newView.didMoveToParentViewController(self)
}
}
}
var currentViewController: UIViewController! {
didSet(oldView) {
oldViewController = oldView
newViewController = nil
}
}
var oldViewController: UIViewController!
func removeViewController() {
if let oldView = oldViewController {
oldView.willMoveToParentViewController(nil)
oldView.view.removeFromSuperview()
oldView.removeFromParentViewController()
oldViewController = nil
}
}
newViewController = NewVIewController()
currentViewController = newViewController
UIView.animateWithDuration(2.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
self.currentViewController.view.frame.origin.x = 0 }) { _ in
self.removeViewController()
}
In the completion of the animation, the old view will be taken care of. But to make my app to be more responsive, I'd like to call removeViewController right away and still keeping a "snapshot" of the oldView so the transition will still look the same.
Should I use some other method for transitioning the views?
It's too late to follow the pan gesture once the swipe event has been fired. Instead consider using uiscreenedgegesturerecognizer
The problem is that this is not how you do an interactive view controller transition. You need to trigger an actual view controller transition (e.g. call presentViewController) and supply a UIViewControllerInteractiveTransitioning object.

Resources