Search bar gets removed from the view hierarchy after custom transition - ios

I have two view controllers embedded into a UINavigationController. The first view controller has a UISearchController set to its navigation item. Here is the full code where I configure the search controller:
private func configureSearchController() {
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
//searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.tintColor = .white
searchController.searchBar.delegate = self
//White search text
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedStringKey.foregroundColor.rawValue: UIColor.white]
//White placeholder
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).attributedPlaceholder = NSAttributedString(string: NSLocalizedString("Search", comment: "search bar placeholder"), attributes: [NSAttributedStringKey.foregroundColor: UIColor.white])
//searchController.searchBar.sizeToFit()
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationController?.navigationBar.prefersLargeTitles = false
//
definesPresentationContext = true
}
I call this method from viewDidLoad.
As mentioned in the question title, I use a navigation controller custom transition. Here is the transition animator's full code.
class RevealViewControllerAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private let animationDuration = 1.5
var operation: UINavigationControllerOperation = .push
var isShowing = true
private weak var storedContext: UIViewControllerContextTransitioning?
var snapshot: UIView?
private lazy var viewOnTopOfSnapshot: UIView? = {
let view = UIView()
view.frame = self.snapshot!.frame
if isShowing {
view.backgroundColor = .clear
} else {
view.backgroundColor = UIColor(white: 0.3, alpha: 0.4)
}
return view
}()
private var backgroundViewBackgroundDarkColor = UIColor(white: 0.2, alpha: 0.4)
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return animationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
storedContext = transitionContext
print ("OPERATION", operation.rawValue)
//If we are presenting a view controller
if isShowing {
let fromVC = transitionContext.viewController(forKey: .from) as! ViewController1
let toVC = transitionContext.viewController(forKey: .to) as! ViewController2
snapshot = UIApplication.shared.keyWindow?.snapshotView(afterScreenUpdates: false)
let containerView = transitionContext.containerView
//Adding a view on top of a snapshot and animating its bacground color
if let snapshot = snapshot, let viewOnTopOfSnapshot = viewOnTopOfSnapshot {
containerView.addSubview(self.snapshot!)
containerView.insertSubview(viewOnTopOfSnapshot, aboveSubview: snapshot)
UIView.animate(withDuration: animationDuration - 1.0, animations: {
viewOnTopOfSnapshot.backgroundColor = self.backgroundViewBackgroundDarkColor
}, completion: nil)
}
containerView.addSubview(toVC.view)
toVC.view.frame = transitionContext.finalFrame(for: toVC)
animate(toView: toVC.view, fromTriggerButton: fromVC.filterButton)
} else {
//If we are dismissing the view controller
let fromVC = transitionContext.viewController(forKey: .from) as! ViewController2
let toVC = transitionContext.viewController(forKey: .to) as! ViewController1
let containerView = transitionContext.containerView
//Animating the background color change to clear
if let viewOnTopOfSnapshot = viewOnTopOfSnapshot {
UIView.animate(withDuration: animationDuration, animations: {
viewOnTopOfSnapshot.backgroundColor = .clear
}, completion: {_ in
self.snapshot?.removeFromSuperview()
viewOnTopOfSnapshot.removeFromSuperview()
})
}
//containerView.addSubview(fromVC.view)
containerView.insertSubview(toVC.view!, belowSubview: snapshot!)
animateDismisss(fromView: fromVC.view, toTriggerButton: fromVC.saveButton)
}
}
//MARK: Animation for pushing
private func animate(toView: UIView, fromTriggerButton button: UIButton) {
let rect = CGRect(x: toView.frame.maxX, y: toView.frame.minY, width: button.frame.width, height: button.frame.height)
let circleMaskPathInitial = UIBezierPath(ovalIn: rect)
let fullHeight = toView.bounds.height
let extremePoint = CGPoint(x: button.center.x, y: button.center.y - fullHeight)
let radius = sqrt((extremePoint.x * extremePoint.x) + (extremePoint.y * extremePoint.y))
let circleMaskPathFinal = UIBezierPath(ovalIn: button.frame.insetBy(dx: -radius - 1000, dy: -radius - 1000))
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.cgPath
toView.layer.mask = maskLayer
let maskLayerAnimation = CABasicAnimation(keyPath: "path")
maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
maskLayerAnimation.duration = animationDuration
maskLayerAnimation.delegate = self
maskLayer.add(maskLayerAnimation, forKey: "path")
}
//MARK: Animation for pop (dismiss)
private func animateDismisss(fromView: UIView, toTriggerButton button: UIButton) {
let rect = CGRect(x: button.frame.origin.x, y: button.frame.midY, width: button.frame.width, height: button.frame.width)
let finalCircleMaskPath = UIBezierPath(ovalIn: rect)
let fullHeight = fromView.bounds.height
let extremePoint = CGPoint(x: button.center.x, y: button.center.y - fullHeight)
let radius = sqrt((extremePoint.x * extremePoint.x) + (extremePoint.y * extremePoint.y))
let initialCircleMaskPath = UIBezierPath(ovalIn: button.frame.insetBy(dx: -radius, dy: -radius))
let maskLayer = CAShapeLayer()
maskLayer.path = finalCircleMaskPath.cgPath
fromView.layer.mask = maskLayer
let maskLayerAnimation = CABasicAnimation(keyPath: "path")
maskLayerAnimation.fromValue = initialCircleMaskPath.cgPath
maskLayerAnimation.toValue = finalCircleMaskPath.cgPath
maskLayerAnimation.duration = 0.8
maskLayerAnimation.delegate = self
maskLayer.add(maskLayerAnimation, forKey: "path")
}
extension RevealFilterViewControllerAnimator : CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let context = storedContext {
storedContext?.completeTransition(!context.transitionWasCancelled)
} else {
storedContext = nil
}
}
}
So, in two words, I get a snapshot of the ViewController1, insert it to the containerView, and on top of it, insert another view which background color I change during the animation. When popping, I get rid of the snapshot and the view, and also insert the ViewController1's view to a containerView.
As I mentioned in the beginning of the question, I have a UISearchController with a search bar in the first view controller.
The problem is, after dismissing the ViewController2 the search controller gets removed from the hierarchy and I get a blank white space. Here is the demonstration for that:
When I print the UISearchController or the search bar on the console, I get the object information, however, as you can see, it disappears from the view hierarchy (In a view hierarchy debugger I can't find it).
Why is this happening, and how this could be solved?

Finally, I figured out what was causing the issue, and once I did, the solution was quite simple. So, the reason why that was happening was that in the ViewController2's viewDidLoad method I was hiding the navigation bar, but I never set it back when popping the view controller.
So, here is the code that I use for the navigation bar in the second view controller (my view controllers look a little bit different but the logic is the same):
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: true)
}
Here is how the animation looking right now (I know, there are some rough edges here, but at least, the problem is solved).

Related

Managing Custom UITabBar View

Following this repo, I created a custom "tabBar" via a separate uiview that sits behind the native uitabbarcontroller tabBar. The custom tabBar has rounded corners and a shadow.
Everything works great except in the instances I push/pop a new uiviewcontroller onto/from the embedded uinavigationcontroller stack, where I hide the tabBar.
My issue is smoothly toggling the custom tabBar to hide/show during these instances.
I've set .isHidden = true/false for the custom tabBar when a uiviewcontroller is pushed/popped, and either the custom tabBar disappears too early or appears too late relative to the native uitabbarcontroller tabBar.
Any guidance would be appreciated.
class TabBarViewController: UITabBarController {
let customTabBarView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = Constants.style.offWhite
view.layer.cornerRadius = 20
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: -8.0)
view.layer.shadowOpacity = 0.12
view.layer.shadowRadius = 10.0
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.isHidden = true
self.tabBar.backgroundColor = .clear
self.tabBar.barStyle = .default
self.tabBar.isTranslucent = true
addCustomTabBarView()
hideTabBarBorder()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
customTabBarView.frame = tabBar.frame
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var newSafeArea = UIEdgeInsets()
newSafeArea.bottom += customTabBarView.bounds.size.height
self.children.forEach({$0.additionalSafeAreaInsets = newSafeArea})
}
//Function invoked when pushing/popping viewControllers onto/from the embedded navigation stack
func toggleCustomTabBarView(shouldHide: Bool) {
self.customTabBarView.isHidden = shouldHide
DispatchQueue.main.async {
UIView.transition(with: self.view, duration: TimeInterval(UINavigationController.hideShowBarDuration), options: .transitionCrossDissolve, animations: {
})
}
}
func addCustomTabBarView() {
customTabBarView.frame = tabBar.frame
view.addSubview(customTabBarView)
view.bringSubviewToFront(self.tabBar)
}
func hideTabBarBorder() {
let tabBar = self.tabBar
tabBar.backgroundImage = UIImage.from(color: .clear)
tabBar.shadowImage = UIImage()
tabBar.clipsToBounds = true
}
}
extension UIImage {
static func from(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
}
https://stackoverflow.com/a/65285566/9466631
Couldn't find anything as a solve for the dummy view behind the tabBar, but this new solution worked for me that avoids using the dummy view altogether and adds a CAShapeLayer instead.

Push animation with views

I'm trying to create an animation where viewOne fills the screen and gets pushed off screen and replaced by viewTwo. My attempt below partially works - viewTwo is behaving like it's supposed to, but where viewOne is supposed to be there's just a white space.
import UIKit
class ViewController: UIViewController {
let viewOne = UIView()
let viewTwo = UIView()
func animate () {
view.sendSubview(toBack: viewOne)
let screenBounds = UIScreen.main.bounds
let finalToFrame = screenBounds
let finalFromFrame = finalToFrame.offsetBy(dx: 0, dy: -screenBounds.size.height)
viewOne.frame = finalToFrame.offsetBy(dx: 0, dy: +screenBounds.size.height)
UIView.animate(withDuration: 0.3, animations: {
self.viewOne.frame = finalToFrame
self.viewTwo.frame = finalFromFrame
}, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(viewOne)
viewOne.widthAnchor.constraint(equalTo: view.widthAnchor)
viewOne.heightAnchor.constraint(equalTo: view.heightAnchor)
viewOne.backgroundColor = UIColor.green
view.addSubview(viewTwo)
viewTwo.widthAnchor.constraint(equalTo: view.widthAnchor)
viewTwo.heightAnchor.constraint(equalTo: view.heightAnchor)
viewTwo.backgroundColor = UIColor.blue
view.sendSubview(toBack: viewTwo)
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(animate))
swipe.direction = .down
view.addGestureRecognizer(swipe)
}
}

Display 2 view controllers at the same time with animation

I'm following this awesome video to create a custom transition for my project, because I'm developing for the iPad, so instead of presenting destination view controller full screen, I want to have it occupy half of the screen like this:
My code of the custom transition class is:
class CircularTransition: NSObject {
var circle = UIView()
var startingPoint = CGPoint.zero {
didSet {
circle.center = startingPoint
}
}
var circleColor = UIColor.white
var duration = 0.4
enum circularTransitionMode: Int {
case present, dismiss
}
var transitionMode = circularTransitionMode.present
}
extension CircularTransition: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
if transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {
var viewCenter = presentedView.center
var viewSize = presentedView.frame.size
if UIDevice.current.userInterfaceIdiom == .pad {
viewCenter = CGPoint(x: viewCenter.x, y: viewSize.height)
viewSize = CGSize(width: viewSize.width, height: viewSize.height)
}
circle = UIView()
circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = circle.frame.size.width / 2
circle.center = startingPoint
circle.backgroundColor = circleColor
circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
containerView.addSubview(circle)
presentedView.center = startingPoint
presentedView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
presentedView.alpha = 0
containerView.addSubview(presentedView)
UIView.animate(withDuration: duration, animations: {
self.circle.transform = CGAffineTransform.identity
presentedView.transform = CGAffineTransform.identity
presentedView.alpha = 1
presentedView.center = viewCenter
}, completion: {(sucess: Bool) in transitionContext.completeTransition(sucess)})
}
} else {
if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
let viewCenter = returningView.center
let viewSize = returningView.frame.size
circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = circle.frame.size.width / 2
circle.center = startingPoint
UIView.animate(withDuration: duration + 0.1, animations: {
self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
returningView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
returningView.center = self.startingPoint
returningView.alpha = 0
}, completion: {(success: Bool) in
returningView.center = viewCenter
returningView.removeFromSuperview()
self.circle.removeFromSuperview()
transitionContext.completeTransition(success)
})
}
}
}
func frameForCircle(withViewCenter viewCenter: CGPoint, size viewSize: CGSize, startPoint: CGPoint) -> CGRect {
let xLength = fmax(startingPoint.x, viewSize.width - startingPoint.x)
let yLength = fmax(startingPoint.y, viewSize.height - startingPoint.y)
let offsetVector = sqrt(xLength * xLength + yLength * yLength) * 2
let size = CGSize(width: offsetVector, height: offsetVector)
return CGRect(origin: CGPoint.zero, size: size)
}
}
And the part of code in my view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let secondVC = segue.destination as! ResultViewController
secondVC.transitioningDelegate = self
secondVC.modalPresentationStyle = .custom
}
// MARK: - Animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transtion.transitionMode = .dismiss
transtion.startingPoint = calculateButton.center
transtion.circleColor = calculateButton.backgroundColor!
return transtion
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transtion.transitionMode = .present
transtion.startingPoint = calculateButton.center
transtion.circleColor = calculateButton.backgroundColor!
return transtion
}
But the controller shows up full screen.
You may try the two different Container View for half of top and bottom.
then give animation on it...
So I have finished creating my answer, It takes a different approach than the other answers so bear with me.
Instead of adding a container view what I figured would be the best way was to create a UIViewController subclass (which I called CircleDisplayViewController). Then all your VCs that need to have this functionality could inherit from it (rather than from UIViewController).
This way all your logic for presenting and dismissing ResultViewController is handled in one place and can be used anywhere in your app.
The way your VCs can use it is like so:
class AnyViewController: CircleDisplayViewController {
/* Only inherit from CircleDisplayViewController,
otherwise you inherit from UIViewController twice */
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func showCircle(_ sender: UIButton) {
openCircle(withCenter: sender.center, radius: nil, resultDataSource: calculator!.iterateWPItems())
//I'll get to this stuff in just a minute
//Edit: from talking to Bright Future in chat I saw that resultViewController needs to be setup with calculator!.iterateWPItems()
}
}
Where showCircle will present your ResultViewController using the transitioning delegate with the circle center at the sending UIButtons center.
The CircleDisplayViewController subclass is this:
class CircleDisplayViewController: UIViewController, UIViewControllerTransitioningDelegate, ResultDelegate {
private enum CircleState {
case collapsed, visible
}
private var circleState: CircleState = .collapsed
private var resultViewController: ResultViewController!
private lazy var transition = CircularTransition()
func openCircle(withCenter center: CGPoint, radius: CGFloat?, resultDataSource: ([Items], Int, String)) {
let circleCollapsed = (circleState == .collapsed)
DispatchQueue.main.async { () -> Void in
if circleCollapsed {
self.addCircle(withCenter: center, radius: radius, resultDataSource: resultDataSource)
}
}
}
private func addCircle(withCenter circleCenter: CGPoint, radius: CGFloat?, resultDataSource: ([Items], Int, String])) {
var circleRadius: CGFloat!
if radius == nil {
circleRadius = view.frame.size.height/2.0
} else {
circleRadius = radius
}
//instantiate resultViewController here, and setup delegate etc.
resultViewController = UIStoryboard.resultViewController()
resultViewController.transitioningDelegate = self
resultViewController.delegate = self
resultViewController.modalPresentationStyle = .custom
//setup any values for resultViewController here
resultViewController.dataSource = resultDataSource
//then set the frame of resultViewController (while also setting endFrame)
let resultOrigin = CGPoint(x: 0.0, y: circleCenter.y - circleRadius)
let resultSize = CGSize(width: view.frame.size.width, height: (view.frame.size.height - circleCenter.y) + circleRadius)
resultViewController.view.frame = CGRect(origin: resultOrigin, size: resultSize)
resultViewController.endframe = CGRect(origin: resultOrigin, size: resultSize)
transition.circle = UIView()
transition.startingPoint = circleCenter
transition.radius = circleRadius
transition.circle.frame = circleFrame(radius: transition.radius, center: transition.startingPoint)
present(resultViewController, animated: true, completion: nil)
}
func collapseCircle() { //THIS IS THE RESULT DELEGATE FUNCTIONS
dismiss(animated: true) {
self.resultViewController = nil
}
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitionMode = .dismiss
transition.circleColor = UIColor.red
return transition
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitionMode = .present
transition.circleColor = UIColor.red
return transition
}
func circleFrame(radius: CGFloat, center: CGPoint) -> CGRect {
let circleOrigin = CGPoint(x: center.x - radius, y: center.y - radius)
let circleSize = CGSize(width: radius*2, height: radius*2)
return CGRect(origin: circleOrigin, size: circleSize)
}
}
public extension UIStoryboard {
class func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) }
}
private extension UIStoryboard {
class func resultViewController() -> ResultViewController {
return mainStoryboard().instantiateViewController(withIdentifier: "/* Your ID for ResultViewController */") as! ResultViewController
}
}
The only function that is called by the VCs that inherit from DisplayCircleViewController is openCircle, openCircle has a circleCenter argument (which should be your button center I'm guessing), an optional radius argument (if this is nil then a default value of half the view height is taken, and then whatever else you need to setup ResultViewController.
In the addCircle func there is some important stuff:
you setup ResultViewController however you have to before presenting (like you would in prepare for segue),
then setup the frame for it (I tried to make it the area of the circle that is visible but it is quite rough here, might be worth playing around with),
then this is where I reset the transition circle (rather than in the transition class), so that I could set the circle starting point, radius and frame here.
then just a normal present.
If you haven't set an identifier for ResultViewController you need to for this (see the UIStoryboard extensions)
I also changed the TransitioningDelegate functions so you don't set the circle center, this is because to keep it generic I put that responsibility to the ViewController that inherits from this one. (see top bit of code)
Finally I changed the CircularTransition class
I added a variable:
var radius: CGFloat = 0.0 //set in the addCircle function above
and changed animateTransition:
(removed the commented out lines):
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
if transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {
...
// circle = UIView()
// circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = radius
...
}
} else {
if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
...
// circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
...
}
}
}
Finally I made a protocol so that ResultViewController could dismiss the circle
protocol ResultDelegate: class {
func collapseCircle()
}
class ResultViewController: UIViewController {
weak var delegate: ResultDelegate!
var endFrame: CGRect!
var dataSource: ([Items], Int, String)! // same as in Bright Future's case
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidLayoutSubviews() {
if endFrame != nil {
view.frame = endFrame
}
}
#IBAction func closeResult(_ sender: UIButton) {
delegate.collapseCircle()
}
}
This has turned out to be quite a huge answer, sorry about that, I wrote it in a bit a of rush so if anything is not clear just say.
Hope this helps!
Edit: I found the problem, iOS 10 has changed the way they layout views, so to fix this I added an endFrame property to ResultViewController and set it's views frame to that in viewDidLayoutSubviews. I also set both the frame and endFrame at the same time in addCircle. I changed the code above to reflect the changes. It's not ideal but I'll have another look later to see if there is a better fix.
Edit: this is what it looks like open for me
Thanks to everyone for the suggestions, I tried to use a container view, here's how I did it:
First I added a containerView property in CircularTransition class:
class CircularTransition: NSObject {
...
var containerView: UIView
init(containerView: UIView) {
self.containerView = containerView
}
...
}
Then commented out these code in its extension:
// let containerView = transitionContext.containerView
// if UIDevice.current.userInterfaceIdiom == .pad {
// viewCenter = CGPoint(x: viewCenter.x, y: viewSize.height)
// viewSize = CGSize(width: viewSize.width, height: viewSize.height)
// }
In my mainViewController, I added a method to add a container view:
func addContainerView() {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
])
transtion.containerView = containerView
}
The reason I don't use story board is, if I put the animated view controller (ResultViewController) in the container view, it gets loaded whenever mainViewController is loaded, however, ResultViewController needs the data from prepareForSegue, thus it'll crash.
Then I changed a little bit in prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
transtion.containerView = view
if UIDevice.current.userInterfaceIdiom == .pad {
addContainerView()
}
let secondVC = segue.destination as! ResultViewController
secondVC.transitioningDelegate = self
secondVC.modalPresentationStyle = .custom
secondVC.dataSource = calculator!.iterateWPItems().0
}
And created CircularTransition class this way in mainViewController:
let transtion = CircularTransition(containerView: UIView())
That's basically all I did, I could display the gorgeous dual vc view
on the iPad, however, the return transition doesn't work, I still
haven't figured out what caused that.
Hi i did some changes in your animateTransition method try this out. You might have to play a little bit with withRelativeStartTime of the animations and the frame and center to perfect the animation. But i guess this should get you started.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
if transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {
var viewCenter = presentedView.center
var viewSize = presentedView.frame.size
if UIDevice.current.userInterfaceIdiom == .pad {
viewCenter = CGPoint(x: viewCenter.x, y: viewSize.height)
viewSize = CGSize(width: viewSize.width, height: viewSize.height)
}
circle = UIView()
circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = circle.frame.size.width / 2
circle.center = startingPoint
circle.backgroundColor = circleColor
circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
circle.layer.masksToBounds = true
containerView.addSubview(circle)
presentedView.center = startingPoint
presentedView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
presentedView.alpha = 0
containerView.addSubview(presentedView)
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
self.circle.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
presentedView.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.19, relativeDuration: 1, animations: {
presentedView.transform = CGAffineTransform(scaleX: 1, y: 1)
presentedView.frame = CGRect(x: 0, y: (containerView.frame.size.height / 2)+10, width: containerView.frame.size.width, height: containerView.frame.size.height*0.5)
})
}, completion: { (sucess) in
transitionContext.completeTransition(sucess)
})
}
} else {
if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
let viewCenter = returningView.center
let viewSize = returningView.frame.size
circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = circle.frame.size.width / 2
circle.center = startingPoint
UIView.animate(withDuration: duration + 0.1, animations: {
self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
returningView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
returningView.center = self.startingPoint
returningView.alpha = 0
}, completion: {(success: Bool) in
returningView.center = viewCenter
returningView.removeFromSuperview()
self.circle.removeFromSuperview()
transitionContext.completeTransition(success)
})
}
}
}
Hope this helps.

Transition Animation like in snapchat

I am trying to do Transition Animation like in snapchat.When user swaps screen in downward direction the video continues to play smoothly.
Image in container While transition is performed
I am adding FromViewController view as transition container subview.
(FromViewController plays video using AVPlayerLayer)
but while transition the video in the container layer (which i have added FromViewController ) lags it does not play smoothly.
containerView!.addSubview((fromVC?.view)!)
I am posting the code below.
class CircleTransitionAnimator: NSObject , UIViewControllerAnimatedTransitioning
{
weak var transitionContext: UIViewControllerContextTransitioning?
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5;
}
#IBAction func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView()
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) //as! ViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) //as! ViewController
containerView!.addSubview((fromVC?.view)!)
let initialFrame = CGRectMake(-fromVC!.view.frame.size.width/2,0, fromVC!.view.frame.size.height, fromVC!.view.frame.size.height ) as CGRect
var endFrame : CGRect = CGRectMake(0, 0, 0, 0)
if toVC?.isKindOfClass(ViewController) == true {
let viewToMove = toVC as! ViewController
let cellForDismiss = viewToMove.cellViewToDismissTo
if cellForDismiss != nil {
endFrame = cellForDismiss.frame as CGRect
}
self.transitionContext?.initialFrameForViewController(fromVC!)
let maskPath = UIBezierPath(rect: initialFrame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toVC!.view.frame;
maskLayer.path = maskPath.CGPath;
let smallCirclePath = UIBezierPath(rect: endFrame)
maskLayer.path = smallCirclePath.CGPath;
fromVC!.view.layer.mask = maskLayer
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = maskPath.CGPath
pathAnimation.toValue = smallCirclePath
pathAnimation.duration = self.transitionDuration(transitionContext)
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0;
opacityAnimation.toValue = 0.5;
opacityAnimation.duration = self.transitionDuration(transitionContext)
CATransaction.begin()
CATransaction.setCompletionBlock({
transitionContext.completeTransition(transitionContext.transitionWasCancelled())
toVC!.view.layer.mask = nil
fromVC!.view.layer.mask = nil
sourceSnap.removeFromSuperview()
})
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
maskLayer.addAnimation(opacityAnimation, forKey: "opacityAnimation")
CATransaction.commit()
}
else{
if fromVC?.isKindOfClass(ViewController) == true {
let viewToMove = fromVC as! ViewController
let cellForDismiss = viewToMove.cellViewToDismissTo
containerView!.addSubview(toVC!.view)
let circleMaskPathInitial = UIBezierPath(rect: cellForDismiss!.frame)
let extremePoint = CGPoint(x: cellForDismiss!.center.x - 0, y: cellForDismiss!.center.y - CGRectGetHeight(toVC!.view.bounds))
let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
let circleMaskPathFinal = UIBezierPath(rect: CGRectInset(cellForDismiss!.frame, -radius, -radius))
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.CGPath
toVC!.view.layer.mask = maskLayer
let maskLayerAnimation = CABasicAnimation(keyPath: "path")
maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath
maskLayerAnimation.toValue = circleMaskPathFinal.CGPath
maskLayerAnimation.duration = self.transitionDuration(transitionContext)
maskLayerAnimation.delegate = self
maskLayer.addAnimation(maskLayerAnimation, forKey: "path")
}
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
}
}
In the other class i am handling pan gesture states.
class NavigationControllerDelegate: NSObject , UINavigationControllerDelegate {
#IBOutlet weak var navigationController: UINavigationController?
var interactionController: UIPercentDrivenInteractiveTransition?
var transitionInProgress : Bool = false
var shouldCompleteTransition : Bool = false
var parentViewController : UIViewController!
var percent : CGFloat!
override func awakeFromNib() {
super.awakeFromNib()
// Added Pan gesture to Navigatin Controller
let panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:"))
self.navigationController!.view.addGestureRecognizer(panGesture)
let controller :[UIViewController] = (self.navigationController?.viewControllers)!
for contro in controller {
if contro.isKindOfClass(ViewController) == true {
parentViewController = contro
}
}
}
#IBAction func panned(gestureRecognizer: UIPanGestureRecognizer) {
let point : CGPoint = gestureRecognizer .translationInView(self.parentViewController.view)
self.percent = CGFloat( fmaxf(fminf(Float(point.y) / 400.0 , 0.99), 0.0))
let velocity : CGPoint = gestureRecognizer.velocityInView(self.parentViewController.view)
// Percent is used for completing transaction , its value tells when the complete transition should start ( after the point when we stop sliding the view down )
switch gestureRecognizer.state {
case .Began:
//
self.transitionInProgress = true;
self.interactionController = UIPercentDrivenInteractiveTransition()
self.navigationController?.popViewControllerAnimated(true)
case .Changed:
// when we use swipe down then this block is continously called and update the interactve transition
self.shouldCompleteTransition = (Double(self.percent) > 0.0 );
let translation = gestureRecognizer.translationInView(self.navigationController!.view)
let completionProgress = translation.x/CGRectGetWidth(self.navigationController!.view.bounds)
self.interactionController?.updateInteractiveTransition(self.percent)
case .Ended:
// When swipe gesture ends , we finish the transition or in any other case "cancel" the transition
self.transitionInProgress = false;
if (self.shouldCompleteTransition == false || gestureRecognizer.state == .Cancelled) {
self.interactionController?.finishInteractiveTransition()
}else{
self.interactionController?.cancelInteractiveTransition()
}
self.interactionController = nil
default:
self.interactionController?.cancelInteractiveTransition()
self.interactionController = nil
}
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CircleTransitionAnimator()
}
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactionController
}
}

Searchbar behind Navigationbar

Given 3 Controllers: A,B,C
A has a hidden navigationbar. Calls Controller B via a StoryboardReference.
Controller B shows Navigationbar on viewDidLoad. It has a searchbar and a collectionView. See screenshot A of my storyboard. Calls controller C if a cell is clicked.
Problem:
If A calls B the searchbar is behind the navigationbar (Screenshot B). It appears with the transition from C to B (Screenshot C).
Navigationbar is already translucent. Any ideas?
EDIT
I realized that my animated transitioning is causing my problem.
Perhaps you can spot the error?
class ZoomInCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var transitionContext: UIViewControllerContextTransitioning?
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else { return
}
guard let fromViewTransitionFromView = fromViewController as? TransitionFromViewProtocol else {
return
}
let imageViewSnapshot = fromViewTransitionFromView.getViewForTransition()
let endFrame = CGRectMake(-CGRectGetWidth(toViewController.view.frame)/2, -CGRectGetHeight(toViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
if let containerView = transitionContext.containerView(){
containerView.addSubview(fromViewController.view)
containerView.addSubview(toViewController.view)
containerView.addSubview(imageViewSnapshot)
}
let maskPath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = maskPath.CGPath
toViewController.view.layer.mask = maskLayer
let quadraticEndFrame = CGRect(x: endFrame.origin.x - (endFrame.height - endFrame.width)/2, y: endFrame.origin.y, width: endFrame.height, height: endFrame.height)
let bigCirclePath = UIBezierPath(ovalInRect: quadraticEndFrame)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = maskPath.CGPath
pathAnimation.toValue = bigCirclePath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = bigCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
}
let scaleImageViewAnimation = {
imageViewSnapshot.frame = quadraticEndFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations have endet
imageViewSnapshot.removeFromSuperview()
toViewController.view.layer.mask = nil
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
I believe I was running in to some similar problems in my app, but running in to difficulties because of all the things you don't have control over. My solution was to put a search icon in the navigationbar, then have the search controller slide down over the navigationbar, keeping it out of the table/scroll view. Here is my implementation (should be complete)
import UIKit
class tvc: UITableViewController, UISearchBarDelegate, UISearchControllerDelegate {
var searchController:UISearchController!
#IBAction func startSearch() {
self.navigationController?.presentViewController(self.searchController, animated: true, completion: {})
}
override func viewDidDisappear(animated: Bool) {
cancelSearch(self)
}
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.delegate = self
searchController.hidesNavigationBarDuringPresentation = false
searchController.loadViewIfNeeded() /* Fixes bug in iOS http://stackoverflow.com/questions/32675001/uisearchcontroller-warning-attempting-to-load-the-view-of-a-view-controller */
definesPresentationContext = true
tableView.sectionIndexBackgroundColor = UIColor.clearColor()
tableView.sectionIndexTrackingBackgroundColor = UIColor.clearColor()
}
extension tvc: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResultsForSearchController(searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
func cancelSearch(sender: AnyObject?) {
if sender!.searchController.active == true {
sender?.searchController.resignFirstResponder()
sender!.navigationController!!.dismissViewControllerAnimated(false, completion: {})
sender!.searchController.searchBar.text = ""
sender!.searchController.active = false
}
}
I think problem is your are either you are not setting frame for imageViewSnapshot or setting wrong frame. As imageViewSnapshot includes the search bar, your have to set the frame such are it goes behind the navigation bar. or imageViewSnapshot should contain only visible area of the fromViewTransitionFromView.

Resources