iOS Swift: How to recreate a Photos App UICollectionView Layout - ios

I am wondering for quiet a while now how to create the iOS Photos App layout. How can I make it so it looks like zooming in to a collection while at the same time the navigation bar shows a back button?
Is it a new view controller which gets pushed onto a UINavigationController? And if so, how exactly do they manage to match the tiles while expanding.
Is there maybe even a 3rd party library which lets me easily recreate such a layout?
Hope you can help me to understand the concept of how this works.

To answer your first question, "Is it a new view controller which gets pushed onto a UINavigationController?". Yes, it is a new view controller. What Apple is using here is a UIViewControllerTransitioningDelegate which allows you to present a custom animation on how a view controller is presented and dismissed.
Now on the second question, "Hope you can help me to understand the concept of how this works." There is no easy way to put it as quite a lot is involved. I have recreated the effect which I will show below but first I need to explain some core principles.
From Apple's docs,
When implementing your transitioning delegate object, you can return different animator objects depending on whether a view controller is being presented or dismissed. All transitions use a transition animator object—an object that conforms to the UIViewControllerAnimatedTransitioning protocol—to implement the basic animations. A transition animator object performs a set of animations over a finite period of time.
In other words, the UIViewControllerTransitioningDelegate expects an animator object which you create that describes how the view controller should be presented and how it should be dismissed. Only two of these delegates methods are of interest to what you want to achieve and these are:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
return animator
}
This asks your delegate for the transition animator object to use when presenting a view controller.
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
This asks your delegate for the transition animator object to use when dismissing a view controller.
Both the PresentAnimator and DismissAnimator object conform to UIViewControllerAnimatedTransitioning. From Apple's docs:
In your animator object, implement the transitionDuration(using:) method to specify the duration of your transition and implement the animateTransition(using:) method to create the animations themselves. Information about the objects involved in the transition is passed to your animateTransition(using:) method in the form of a context object. Use the information provided by that object to move the target view controller’s view on or off screen over the specified duration.
Basically, each animator object will describe the duration of the view controller's animation and how it will be animated.
Now here is a demonstration of all this. This is what we will achieve:
Create two view controllers in your storyboard. My first view controller is called ViewController which contains a Collection View and a Collection View cell with an identifier "MediaCell" and an image that fills that collection view cell. The collection view cell has a class called ImageCollectionViewCell with only this:
class ImageCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var image: UIImageView! //links to the collection view cell's image
}
My second view controller is called ImageRevealViewController which simply has a single image view and a grey view at the top that I am using to simulate a navigation bar and a custom back button (I have tried all this with a normal UINavigationController nav bar but the dismiss animator fails to work. There is no shame through is making something that looks and acts like a navigation bar although mine is just for demo).
The Photo Album
This will be the code for your ViewController. Basically this will be the place the user finds a collection of photos just like the Photo Album. I used two test images for mine as you will see.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
var selectedCell = UICollectionViewCell() //the selected cell, important for the animator
var media: [UIImage] = [UIImage]() //the photo album's images
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
media.append(UIImage(named: "testimage1")!)
media.append(UIImage(named: "testimage2")!)
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItem(at: indexPath)!
let selectedCellImage = selectedCell as! ImageCollectionViewCell
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let imageRevealVC = mainStoryboard.instantiateViewController(withIdentifier: "ImageRevealVC") as! ImageRevealViewController
imageRevealVC.transitioningDelegate = self
imageRevealVC.imageToReveal = selectedCellImage.image.image
/*
This is where I tried using the nav controller but things did not work out for the dismiss animator. I have commented it out.
*/
//let navController = UINavigationController(rootViewController: imageRevealVC)
//navController.transitioningDelegate = self
//navigationController?.pushViewController(imageRevealVC, animated: true)
present(imageRevealVC, animated: true, completion: nil)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return media.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MediaCell", for: indexPath) as! ImageCollectionViewCell
cell.image.image = media[indexPath.row]
cell.image.contentMode = .scaleAspectFill
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemsPerRow:CGFloat = 3
let hardCodedPadding:CGFloat = 2
let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding
let itemHeight = itemWidth
return CGSize(width: itemWidth, height: itemHeight)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
animator.originFrame = selectedCell.frame //the selected cell gives us the frame origin for the reveal animation
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
}
The UIViewControllerTransitioningDelegate is at the end alongside the animator objects I talked about. Notice in the didSelect of the collection view that I instantiate the new view controller and make its transitioning delegate equal to self.
The Animators
There are always three steps to making an animator.
Setup the transition
Create the animations
Complete the transitions
Now for the Present Animator. Create a new Swift class called PresentAnimator and add the following:
import Foundation
import UIKit
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
var originFrame = CGRect.zero
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
//2) create animation
let finalFrame = toView.frame
let xScaleFactor = originFrame.width / finalFrame.width
let yScaleFactor = originFrame.height / finalFrame.height
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
toView.transform = scaleTransform
toView.center = CGPoint(
x: originFrame.midX,
y: originFrame.midY
)
toView.clipsToBounds = true
containerView.addSubview(toView)
UIView.animate(withDuration: duration, delay: 0.0,
options: [], animations: {
toView.transform = CGAffineTransform.identity
toView.center = CGPoint(
x: finalFrame.midX,
y: finalFrame.midY
)
}, completion: {_ in
//3 complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
Now for the Dismiss Animator. Create a new class called DismissAnimator and add the following:
import Foundation
import UIKit
class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//1) setup the transition
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
containerView.insertSubview(toView, belowSubview: fromView)
//2) animations!
UIView.animate(withDuration: duration, delay: 0.0, options: [], animations: {
fromView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
}, completion: {_ in
//3) complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
The Image Revealed
Now for the final step, the view controller that reveals the image. In your ImageRevealController add this:
import UIKit
class ImageRevealViewController: UIViewController {
var imageToReveal: UIImage!
#IBOutlet weak var imageRevealed: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageRevealed.image = imageToReveal
}
#IBAction func backButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
The backButton connects to the button that I added to the view that acts like nav bar. You can add your own back indicator to make it more authentic.
For more info on UIViewControllerTransitioningDelegate there is a section here "From View Controller" disappears using UIViewControllerContextTransitioning, you could look into and to which I have contributed an answer.

To create a Photo Gallery, you can refer to : https://github.com/inspace-io/INSPhotoGallery.
It is a nice library for showing photos, zooming funcationality and many more.

Related

Zoom animation with a UICollectionView inside a UINavigationController

I'm trying to animate the transition from a UICollectionViewController to a UIViewController.
TLDR: in the last function, how can I get the frame that will take the navigation bar into account?
Each cell has a UIImageView, and I want to zoom in on it as I transition to the view controller. This is similar to the Photos app, except that the image is not centered and there are other views around (labels, etc.).
The UIImageView is placed using AutoLayout, centered horizontally and 89pts from the top safe layout guide.
It almost works, except that I can't manage to get the proper frame for the target UIImageView, it doesn't take into account the navigation and so the frame y origin is off by as much.
My UICollectionViewController is the UINavigationControllerDelegate
class CollectionViewController: UINavigationControllerDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard segue.identifier == detailSegue,
let destination = segue.destination as? CustomViewController,
let indexPath = sender as? IndexPath else { return }
navigationController?.delegate = self
…
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return ZoomInAnimator()
default:
return nil
}
}
}
The ZoomInAnimator object looks like that:
open class ZoomInAnimator: NSObject, UIViewControllerAnimatedTransitioning {
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from),
let fromContent = (fromViewController as? ZoomAnimatorDelegate)?.animatedContent(),
let toViewController = transitionContext.viewController(forKey: .to),
let toContent = (toViewController as? ZoomAnimatorDelegate)?.animatedContent() else { return }
let finalFrame = toContent.pictureFrame!
// Set pre-animation state
toViewController.view.alpha = 0
toContent.picture.isHidden = true
// Add a new UIImageView where the "from" picture currently is
let transitionImageView = UIImageView(frame: fromContent.pictureFrame)
transitionImageView.image = fromContent.picture.image
transitionImageView.contentMode = fromContent.picture.contentMode
transitionImageView.clipsToBounds = fromContent.picture.clipsToBounds
transitionContext.containerView.addSubview(toViewController.view)
transitionContext.containerView.addSubview(transitionImageView)
// Animate the transition
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0,
options: [.transitionCrossDissolve],
animations: {
transitionImageView.frame = finalFrame
toViewController.view.alpha = 1
}) { completed in
transitionImageView.removeFromSuperview()
toContent.picture.isHidden = false
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
As you can see, I ask both controllers for their UIImageViews and their frames using animatedContent(). This function is defined in both the UICollectionViewController and the UIViewController, which both conforms to this protocol:
protocol ZoomAnimatorDelegate {
func animatedContent() -> AnimatedContent?
}
With AnimatedContent being a simple struct:
struct AnimatedContent {
let picture: UIImageView!
let pictureFrame: CGRect!
}
Here's how that function is implemented on both sides.
This side works fine (the UICollectionViewController / fromViewController in the transition):
extension CollectionViewController: ZoomAnimatorDelegate {
func animatedContent() -> AnimatedContent? {
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else { return nil }
let cell = cellOrPlaceholder(for: indexPath) // Custom method to get the cell, I'll skip the details
let frame = cell.convert(cell.picture.frame, to: view)
return AnimatedContent(picture: cell.picture, pictureFrame: frame)
}
}
This is the problematic part.
The picture frame origin is 89pts and ignores the navigation bar which adds a 140pt offset. I tried all sorts of convert(to:) without any success. Also, I'm a bit concerned about calling view.layoutIfNeeded here, is it the way to do it?
class ViewController: ZoomAnimatorDelegate {
#IBOutlet weak var picture: UIImageView!
func animatedContent() -> AnimatedContent? {
view.layoutIfNeeded()
return AnimatedContent(picture: picture, profilePictureFrame: picture.frame)
}
}
EDIT: Here's an updated version of animateTransition (not using snapshots yet, but I'll get there)
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from) as? CollectionViewController,
let fromContent = fromViewController.animatedContent(),
let toViewController = transitionContext.viewController(forKey: .to) as? ViewController,
let toImageView = toViewController.picture else { return }
transitionContext.containerView.addSubview(toViewController.view)
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
toViewController.view.setNeedsLayout()
toViewController.view.layoutIfNeeded()
let finalFrame = toViewController.view.convert(toImageView.frame, to: transitionContext.containerView)
let transitionImageView = UIImageView(frame: fromContent.pictureFrame)
transitionImageView.image = fromContent.picture.image
transitionImageView.contentMode = fromContent.picture.contentMode
transitionImageView.clipsToBounds = fromContent.picture.clipsToBounds
transitionContext.containerView.addSubview(transitionImageView)
// Set pre-animation state
toViewController.view.alpha = 0
toImageView.isHidden = true
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0,
options: [.transitionCrossDissolve],
animations: {
transitionImageView.frame = finalFrame
toViewController.view.alpha = 1
}) { completed in
transitionImageView.removeFromSuperview()
toImageView.isHidden = false
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
The problem is that you are obtaining the final frame incorrectly:
let finalFrame = toContent.pictureFrame!
Here's what you should have done:
Start by asking the transition context for the final frame of the to view controller’s view, by calling finalFrame(for:). The fact that you never call that method is a clear bug; you should always call it!
Now use that as either the frame to animate to (if you know that the image will fill it completely), or use it as the basis of the calculation of that frame.
What I do in the latter case is to perform a dummy layout operation to learn what the image frame will be within the view controller’s view, and then convert the coordinate system to that of the transition context. The fact that I don't see you performing any coordinate system conversion is a sign of another bug in your code. Remember, frame is calculated in terms of a view's superview's coordinate system.
Here’s an example from one of my apps; it’s a presented VC not a pushed VC, but that makes no difference to the calculation:
Observe how I know where the red swatch will be in the final frame of the presented view controller's view, and I move the red swatch snapshot to that frame in terms of the transition context's coordinate system.
Also note that I use a snapshot view as a proxy. You are using a loose image view as a proxy and that might be fine too.

How to "wait' for a PopoverPresentationController to be dismissed

I am writing a "notebook" style program. Notebooks have multiple pages and I am trying to put in a "go to page" popover to allow the user to go to any page. The popover presents a collectionView of thumbnails of each page. Obviously, it would be great to:
Click on desired page thumbnail in popover
Send the selectedPage via delegate
Have the recipient mainViewController display the selectedPage
The popover works great. The page selection works and passes the selectedPage back.
The problem is that I need to have the mainViewController "wait" for the user to select a page. Here the relevant sections of code in the mainViewController:
User selects barButton to show popover:
#IBAction func selectPage(sender: UIBarButtonItem) {
self.performSegueWithIdentifier("showPages", sender: self)
//need to wait here for the popover to be dismissed.
//the next line is executed before segue even appears
//"while" and delay don't work
imageView.image = currentNotebook.pages[goToPage! - 1] //displays selectedPage (goToPage is set by delegation)
goToPage = nil
}
//segue to popover
case "showPages" :
let navigationController = segue.destinationViewController as? PageCollectionViewController
if let vc = navigationController {
vc.delegate = self
vc.modalInPopover = false
vc.preferredContentSize = CGSizeMake(400,100)
vc.notebook = self.currentNotebook
print("got to show pages")
}
I suspect I need some sort of closure or handler in the selectPage function, but I can't figure it out. Hope that is clear enough. It is very early in the morning...
Here is the code for the popover:
import UIKit
protocol PageCollectionViewControllerDelegate {
func selectsPage(selectedPage:Int)
}
class PageCollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var notebook: Notebook!
var pageNum: Int!
var delegate: PageCollectionViewControllerDelegate?
#IBOutlet weak var pageCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.pencilCollectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInCollectionView(pageCollectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return notebook.pages.count
}
func collectionView(pageCollectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = pageCollectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! PageCollectionViewCell
let cellImage = notebook.pages[indexPath.row]
let tempThumb = imageWithImage(cellImage, scaledToFillSize: CGSizeMake(cell.bounds.width, cell.bounds.height)) //create thumbnail of each page
cell.pageThumb.image = tempThumb
cell.backgroundColor = UIColor.whiteColor()
return cell
}
func collectionView(pageCollectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
//return page number
if let delegate = self.delegate {
pageNum = indexPath.row + 1
print("page number",pageNum)
delegate.selectsPage(pageNum)
}
self.dismissViewControllerAnimated(true, completion: nil) //if you comment this out the popover is not dismissed when clicking on a cell
}
func imageWithImage(image: UIImage, scaledToFillSize size: CGSize) -> UIImage {
let scale: CGFloat = max(size.width / image.size.width, size.height / image.size.height)
let width: CGFloat = image.size.width * scale
let height: CGFloat = image.size.height * scale
let imageRect: CGRect = CGRectMake((size.width - width) / 2.0, (size.height - height) / 2.0, width, height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
image.drawInRect(imageRect)
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
This is a typical example of async programming. In this case, the async event you're waiting for is a user response.
The almost universal answer is "don't wait. Send a message and have the event notify you when it's done."
What I would do is create a custom subclass of UIViewController as your popover, and either give the popover a completion block (closure) property, or set it up with a delegate, and define a protocol so that the popover can notify it's delegate when the user selects an option. (The two approaches are quite similar, but with a completion block you don't have to define a method that gets invoked - you just pass in your completion code when you make the call that invokes the popover.)

UINavigationBar below StatusBar after Hotspot/Call when using custom transition

I've a strange issue with the NavigationBar behind the Statusbar.
It only occurs when the default statusbar changes to an "active" statusbar like the one that appears during an active call or a wifi hotspot.
Before the "active" statusbar appears, it looks like this (which is perfectly fine):
When I enable the wifi hotspot it's still fine:
But when I disable the wifi hotspot or end a call the statusbar size shrinks back to its previous size but the ViewController (in this case a UITableViewController) doesn't move up again. It looks like it has a top margin of the statusbars' size. In addition the statusbar is transparent and I can see the background of the view controller below the actual table view controller.
Any ideas on this issue?
Update:
I figured out that it's because of a custom modal transition that I've implemented.
It should be a dissolve animation.
That's the code:
class DissolveTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
// vars
private var duration: NSTimeInterval = 0.3
private var presenting = true
// MARK: - UIViewControllerAnimatedTransitioning
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return self.duration
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let destination = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
if (destination?.isBeingPresented() == true) {
self.animatePresentation(transitionContext)
}
else {
self.animateDismissal(transitionContext)
}
}
private func animatePresentation(transitionContext: UIViewControllerContextTransitioning) {
self.animateDissolve(transitionContext)
}
private func animateDismissal(transitionContext: UIViewControllerContextTransitioning) {
self.presenting = false
self.animateDissolve(transitionContext)
}
private func animateDissolve(transitionContext: UIViewControllerContextTransitioning) {
let source = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let destination = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let container = transitionContext.containerView()!
destination.beginAppearanceTransition(true, animated: true)
let snapshotFromView = source.view.snapshotViewAfterScreenUpdates(true)
// 1. adding real view at the bottom of the view hierarchy
if (self.presenting) {
container.addSubview(destination.view)
}
// 2. adding snapshot of previous view to view hierarchy
container.addSubview(snapshotFromView)
// 3. removing (fade) prev snapshot view and show real VC
UIView.animateWithDuration(self.duration, animations: {
snapshotFromView.alpha = 0.0
}, completion: { (finished) in
if (finished) {
snapshotFromView.removeFromSuperview()
container.bringSubviewToFront(destination.view)
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
destination.endAppearanceTransition()
})
}
// MARK: - UIViewControllerTransitioningDelegate
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
I found out that it was because of my custom modal transition that presented this view.
There is an odd bug in iOS that views inside the screen are not resized after the statusBar is changed. This also appears in many well-known Apps.
I fixed it by resizing the views when the statusbar-size changes.
Use the following code in your AppDelegate:
func application(application: UIApplication, willChangeStatusBarFrame newStatusBarFrame: CGRect) {
if (newStatusBarFrame.size.height < 40) {
if let window = self.window, subviews = self.window?.subviews {
for view in subviews {
UIView.animateWithDuration(0.3, animations: {
view.frame = window.bounds
})
}
}
}
}

Swift - How to do a custom slide animation?

I've been looking for swift code to make simple custom slide transitions between views (just left to right or right to left, without bounce) but I only found code for complicated animations. Thanks everyone for your help !
Oscar
I finally found the answer here : http://mathewsanders.com/animated-transitions-in-swift/#custom-transition-animations and adpated it a little bit.
1) Create this Swift NSObject file
class TransitionManager2: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = true
// MARK: UIViewControllerAnimatedTransitioning protocol methods
// animate a change from one viewcontroller to another
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// set up from 2D transforms that we'll use in the animation
let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
// prepare the toView for the animation
toView.transform = self.presenting ? offScreenRight : offScreenLeft
// set the anchor point so that rotations happen from the top-left corner
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)
// updating the anchor point also moves the position to we have to move the center position to the top-left to compensate
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: nil, animations: {
// slide fromView off either the left or right edge of the screen
// depending if we're presenting or dismissing this view
fromView.transform = self.presenting ? offScreenLeft : offScreenRight
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
// return how many seconds the transiton animation will take
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.4
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
// remmeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// these methods are the perfect place to set our `presenting` flag to either true or false - voila!
self.presenting = true
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
}
2) Change the segue between the 2 ViewControllers to "Custom"
3) Add in the first ViewController this code :
let transitionManager = TransitionManager2()
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// this gets a reference to the screen that we're about to transition to
let toViewController = segue.destinationViewController as! UIViewController
// instead of using the default transition animation, we'll ask
// the segue to use our custom TransitionManager object to manage the transition animation
toViewController.transitioningDelegate = self.transitionManager
}
What you need to do is subclass UIStoryboardSegue Class and override the perform method.
The code inside your perform method would be something like this
var ourOriginViewController = self.sourceViewController as! UIViewController
ourOriginViewController.navigationController?.pushViewController(self.destinationViewController as! UIViewController, animated: false)
var transitionView = ourOriginViewController.navigationController?.view
UIView.transitionWithView(transitionView!, duration: 1, options: UIViewAnimationOptions.TransitionFlipFromRight, animations: { () -> Void in
}) { (success) -> Void in
}
Assign this segue class to your custom segue in storyboard
Attaching screenshot for reference

iOS Swift: UIViewControllerAnimatedTransitioning end frame in wrong position

I have a Swift project, learning a weather API and also trying to get a better handle on the AnimatedTransitions. I have a UITableView using a custom UITableViewCell with images and text. Tapping a cell in the tableView transitions to a new UIViewController as a Show (push), with the whole thing embedded in a UINavigationController.
When the transition is invoked, the image from the cell is supposed to move to the final location of the UIImageView on the destination viewController. However, what it does is move past that point to the far side of the screen before the transition completes and the view changes, making the image appear to snap back to the center of the view.
I have read a lot of tutorials trying to fix this and have read a lot of StackOverflow but have failed to figure it out. Can someone point out to me what I have missed, please? I'm going crazy, here.
The segue that invokes the transition, in the original ViewController:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("SHOW_DETAIL", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SHOW_DETAIL" {
let detailVC = segue.destinationViewController as DetailViewController
let indexPathForForecast = self.tableView.indexPathForSelectedRow() as NSIndexPath!
let detailForecast = self.forecasts?[indexPathForForecast.row]
let cell = self.tableView.cellForRowAtIndexPath(indexPathForForecast) as WeatherCell
let image = cell.forecastImage.image
detailVC.forecastForDetail = detailForecast
detailVC.forecastDetailImage = image
}
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if fromVC == self && toVC.isKindOfClass(DetailViewController) {
let transitionVC = AnimateToDetailVCController()
return transitionVC
} else {
return nil
}
}
And here's the animateTransition code from the UIViewControllerAnimatedTransitioning object (EDIT: solution code edited into code block, thanks #jrturton!)
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as ViewController
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as DetailViewController
let containerView = transitionContext.containerView()
let duration = self.transitionDuration(transitionContext)
let selectedRow = fromViewController.tableView.indexPathForSelectedRow()
let cell = fromViewController.tableView.cellForRowAtIndexPath(selectedRow!) as WeatherCell
let weatherSnapshot = cell.forecastImage.snapshotViewAfterScreenUpdates(false)
weatherSnapshot.frame = containerView.convertRect(cell.forecastImage.frame, fromView: fromViewController.tableView.cellForRowAtIndexPath(selectedRow!)?.superview)
cell.forecastImage.hidden = true
toViewController.view.frame = transitionContext.finalFrameForViewController(toViewController)
toViewController.view.alpha = 0
toViewController.detailImageView.hidden = true
containerView.addSubview(toViewController.view)
containerView.addSubview(weatherSnapshot)
var toFrame = toViewController.locationIs
UIView.animateWithDuration(duration, animations: { () -> Void in
// EDIT: This solved the issue, thanks JRTurton!
toViewController.view.setNeedsLayout() // Solution: This is where it was needed
toViewController.view.layoutIfNeeded() // Solution: This is where it was needed
toViewController.view.alpha = 1.0
var endRect = containerView.convertRect(toViewController.detailImageView.frame, fromView: toViewController.view)
weatherSnapshot.frame = endRect
}) { (finished) -> Void in
toViewController.detailImageView.hidden = false
cell.forecastImage.hidden = false
weatherSnapshot.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
It's hard to say, but here's my guess: you're using size classes, designing at the any/any size, and the image in your to view controller is still centered in respect of that when you get its frame to use for your animation, making it too far to the right. Once the transition is complete, a layout pass happens and it gets corrected.
To fix, after you set the frame of the to view controller, force a layout pass:
toViewController.view.setNeedsLayout()
toViewController.view.layoutIfNeeded()
Before making the above change, you can first confirm if this is the issue by checking the image view's frame before the animation.

Resources