When is the destination view controller initialized during a segue? - ios

I'm attempting to write a protocol and a custom UIStoryboardSegue class that will allow me to easily implement custom transitions in my UIViewControllers:
public protocol TransitionController
{
var transitionDurationIn: CFTimeInterval { get }
var transitionDurationOut: CFTimeInterval { get }
func prepareTransitionIn()
func prepareTransitionOut()
func performTransitionIn(finished: #escaping () -> Void)
func performTransitionOut(finished: #escaping () -> Void)
}
class JFTransitionControllerSegue: UIStoryboardSegue {
override func perform() {
let defaultTransitionDuration : CFTimeInterval = 1.5
if let dvc = self.destination as? TransitionController {
dvc.prepareTransitionIn()
}
else {
// Default transition
self.destination.view.alpha = 0
}
if let svc = self.source as? TransitionController {
svc.prepareTransitionOut()
svc.performTransitionOut(){ () in
if let dvc = self.destination as? TransitionController {
dvc.performTransitionIn(){ () in
self.source.present(self.destination, animated: false, completion: nil)
}
}
else {
// Default transition for the destination controller
UIView.animate(withDuration: defaultTransitionDuration, animations: {
self.destination.view.alpha = 1
}) { (Finished) in
self.source.present(self.destination, animated: false, completion: nil)
}
}
}
}
else
{
// Default transition for the source controller
UIView.animate(withDuration: defaultTransitionDuration, animations: {
self.source.view.alpha = 0
}) { (Finished) in
if let dvc = self.destination as? TransitionController {
dvc.performTransitionIn(){ () in
self.source.present(self.destination, animated: false, completion: nil)
}
}
else {
// Default transition for the destination controller
UIView.animate(withDuration: defaultTransitionDuration, animations: {
self.destination.view.alpha = 1
}) { (Finished) in
self.source.present(self.destination, animated: false, completion: nil)
}
}
}
}
}
}
class TestController: UIViewController, TransitionController {
#IBOutlet weak var form_username: UITextField!
// MARK: - TransitionController Protocol
var transitionDurationIn : CFTimeInterval {return 1.0}
var transitionDurationOut : CFTimeInterval {return 1.0}
func prepareTransitionIn()
{
//self.view.alpha = 0 // no fade in if you uncomment
form_username.alpha = 0 // nil
}
func prepareTransitionOut()
{
self.view.alpha = 1 // works
}
func performTransitionIn(finished: #escaping () -> Void)
{
UIView.animate(withDuration: self.transitionDurationIn, animations: {
//self.view.alpha = 1 // no fade in if you uncomment
self.form_username.alpha = 1 // nil, crashes
}) { (Finished) in
finished()
}
}
func performTransitionOut(finished: #escaping () -> Void)
{
UIView.animate(withDuration: self.transitionDurationOut, animations: {
self.view.alpha = 0 // fades out correctly
}) { (Finished) in
finished()
}
}
}
Basically, you just implement the protocol in any UIViewController you want, then make a segue of class JFTransitionControllerSegue. In the performTransitionIn function, you can just do something like UIView.animate and change the alpha or whatever you like. The problem I'm having is that the destination segue simply pops up instead of transitioning in properly. From what I can tell while debugging it isn't fully initialized - IBOutlet variables are nil, but the controller itself isn't. Is this a bad design pattern, or am I just missing something simple?

A view controller being initialised is one event. It's view being loaded is another.
The view property of a view controller is loaded lazily and the outlets are built and connected at that point. That's why viewDidLoad() is a thing.
If you want the view to be ready for you, you can call loadViewIfNeeded() on the view controller first.

Misdiagnosed the problem...the destination controller was loaded, but I had forgotten to add the destination controller's view to the window in the Segue class:
class JFTransitionControllerSegue: UIStoryboardSegue {
override func perform() {
let defaultTransitionDuration : CFTimeInterval = 1.5
if let dvc = self.destination as? TransitionController {
dvc.prepareTransitionIn()
}
else {
// Default transition
self.destination.view.alpha = 0
}
if let svc = self.source as? TransitionController {
svc.prepareTransitionOut()
svc.performTransitionOut(){ () in
UIApplication.shared.keyWindow?.insertSubview(self.destination.view, aboveSubview: self.source.view)
if let dvc = self.destination as? TransitionController {
dvc.performTransitionIn(){ () in
self.source.present(self.destination, animated: false, completion: nil)
}
}
else {
// Default transition for the destination controller
UIView.animate(withDuration: defaultTransitionDuration, animations: {
self.destination.view.alpha = 1
}) { (Finished) in
self.source.present(self.destination, animated: false, completion: nil)
}
}
}
}
else
{
// Default transition for the source controller
UIView.animate(withDuration: defaultTransitionDuration, animations: {
self.source.view.alpha = 0
}) { (Finished) in
UIApplication.shared.keyWindow?.insertSubview(self.destination.view, aboveSubview: self.source.view)
if let dvc = self.destination as? TransitionController {
dvc.performTransitionIn(){ () in
self.source.present(self.destination, animated: false, completion: nil)
}
}
else {
// Default transition for the destination controller
UIView.animate(withDuration: defaultTransitionDuration, animations: {
self.destination.view.alpha = 1
}) { (Finished) in
self.source.present(self.destination, animated: false, completion: nil)
}
}
}
}
}
}

Related

Push view controller over PHPickerViewController

Is that possible to push vc over the PHPickerViewController?
I'm trying to do that like this with no luck:
var configuration = PHPickerConfiguration()
configuration.filter = .any(of: [.images, .livePhotos])
photoPickerController = PHPickerViewController(configuration: configuration)
photoPickerController.delegate = self
present(self.photoPickerController, animated: true)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
//Push segue
performSegue(withIdentifier: "showAddPost", sender: self)
}
Update - Using CocoaPods
I created a simple pod PhotoCropController that includes a basic photo crop controller to be presented from the PHPickerViewControllerDelegate. It provides transitions to push to a modally presented controller and to pop or dismiss. The aspect ratio of the crop view can be edited as well. To use conform your view controller to the PhotoCropDelegate protocol and present the PhotoCropController from your PHPickerViewControllerDelegate. Implementation would look something like the following:
extension ViewController: PHPickerViewControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, PhotoCropDelegate {
func browsePhotoLibrary() {
if #available(iOS 14, *) {
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
config.selectionLimit = 1
config.preferredAssetRepresentationMode = .compatible
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
let nav = UINavigationController(rootViewController: picker)
nav.setNavigationBarHidden(true, animated: false)
nav.setToolbarHidden(true, animated: true)
present(nav, animated: true) } else {
let picker = UIImagePickerController()
picker.delegate = self
picker.allowsEditing = true
present(picker, animated: true)
}
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
image = edited
} else if let selected = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
image = selected
}
presentedViewController?.dismiss(animated: true)
}
#available(iOS 14, *)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
if let provider = results.last?.itemProvider,
provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { [weak self] result, error in
if let image = result as? UIImage {
DispatchQueue.main.async { self?.select(image: image) }
} else if let error = error {
NSLog("Error picking image: %#", error.localizedDescription)
DispatchQueue.main.async { picker.dismiss(animated: true) }
}
}
} else { DispatchQueue.main.async { picker.dismiss(animated: true) } }
}
func select(image: UIImage) {
let destinationSize = AVMakeRect(aspectRatio: image.size, insideRect: view.frame).integral.size
//Best Performance
let resizedImage = UIGraphicsImageRenderer(size: destinationSize).image { (context) in
image.draw(in: CGRect(origin: .zero, size: destinationSize))
}
let cropController = PhotoCropController()
cropController.delegate = self
cropController.image = resizedImage
presentedViewController?.present(cropController, animated: true)
}
#objc func cropViewDidCrop(image: UIImage?) {
self.image = image
presentedViewController?.dismiss(animated: true) { [weak self] in
self?.presentedViewController?.dismiss(animated: true)
}
}
}
To present another controller modally in front of the PHPickerViewController:
The presentedViewController property of your view controller will be your photoPickerController so you can present another controller in front of it as follows:
present(photoPickerController, animated: true)
presentedViewController?.present(yourViewController, animated: true)
If dismissing from the presentedViewController, you will need to call twice, once to dismiss yourViewController and then again to dismiss the photoPickerController, if they are both presented:
presentedViewController?.dismiss(animated: true)
presentedViewController?.dismiss(animated: true)
Pushing a custom controller on top of the navigation stack
To create the appearance of pushing to the PHPickerViewController stack you can use custom UIPresentationController and UIViewControllerAnimatedTransitioning classes to present your view. The following classes will mimic a push to a modally presented navigation controller:
import UIKit
import PhotosUI
class SlideInModalPresentationController: UIPresentationController {
var offset: CGFloat = 0.0
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView?.frame.origin.y = offset
presentedView?.frame.size.height -= offset
presentedView?.layer.cornerRadius = 10
presentedView?.clipsToBounds = true
}
}
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
private let duration = 0.3
var isPresenting: Bool = true
var dismissModally: Bool = false
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toController = transitionContext.viewController(forKey: .to),
let fromController = transitionContext.viewController(forKey: .from)
else { return}
if isPresenting {
toController.view.frame.origin.x = fromController.view.frame.width
transitionContext.containerView.addSubview(toController.view)
UIView.animate(withDuration: duration, animations: {
toController.view.frame.origin.x = 0
}, completion: { _ in
transitionContext.completeTransition(true)
})
} else if dismissModally {
var stack: UIView? = nil
if #available(iOS 14, *), toController is PHPickerViewController {
stack = toController.view.superview
toController.dismiss(animated: false)
} else if toController is UIImagePickerController {
stack = toController.view.superview
toController.dismiss(animated: false)
}
UIView.animate(withDuration: duration, animations: {
stack?.frame.origin.y = fromController.view.frame.height
fromController.view.frame.origin.y = fromController.view.frame.height
}, completion: { _ in
transitionContext.completeTransition(true)
fromController.view.removeFromSuperview()
})
} else {
UIView.animate(withDuration: duration, animations: {
fromController.view.frame.origin.x = fromController.view.frame.width
}, completion: { _ in
transitionContext.completeTransition(true)
fromController.view.removeFromSuperview()
})
}
}
}
To implement in your view controller:
class ViewController: UIViewController {
let slidInTransition = SlideInTransition()
}
extension ViewController: UIViewControllerTransitioningDelegate {
private func presentYourController(_ image: UIImage) {
let yourController = YourController()
yourController.image = image
yourController.modalPresentationStyle = .custom
yourController.transitioningDelegate = self
slidInTransition.dismissModally = false
presentedViewController?.present(yourController, animated: true)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = SlideInModalPresentationController(presentedViewController: presented, presenting: presenting)
presentationController.offset = view.convert(source.view.frame, to: nil).origin.y + 10
return presentationController
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
slidInTransition.isPresenting = true
return slidInTransition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
slidInTransition.isPresenting = false
return slidInTransition
}
private func dismissPhotoStack() {
slidInTransition.dismissModally = true
presentedViewController?.dismiss(animated: true)
}
}
When you are ready to dismiss the whole stack you can call dismissPhotoStack.

AVCaptureVideoPreviewLayer Delay when switching from Video & taking capturing image in still image mode

I Am using a library for creating custom camera on my swift app using the camera and video https://github.com/yonat/CameraBackground
But I am getting a delay & the camera will take an image using the flash when the flash setting is off
when switching my camera mode from video to still image and I'm trying to take an image after changing the outputs
unfortunately, if I don't set a delay it will return a dark image.
The code:
#IBAction func startCapture(_ sender: Any) {
DispatchQueue.background(delay: 0.1, background: {
// do something in background
let currentSession = self.previewLiveCamera.cameraLayer?.session
currentSession!.sessionPreset = AVCaptureSession.Preset.photo
currentSession!.addOutput(AVCaptureStillImageOutput())
}, completion: {
// when background job finishes, wait 3 seconds and do something in main thread
self.previewLiveCamera.takeCameraSnapshot( {
// animate snapshot capture
print("Starting...")
},
completion: { (capturedImage, error) -> () in
if capturedImage != nil {
print("compleated with image")
RZTransitionsManager.shared().defaultPresentDismissAnimationController = RZZoomAlphaAnimationController()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nextViewController = storyboard.instantiateViewController(withIdentifier: "post") as! PostViewController
nextViewController.image = capturedImage
nextViewController.transitioningDelegate = RZTransitionsManager.shared()
self.present(nextViewController, animated: true, completion: {
self.previewLiveCamera.removeCameraBackground()
self.addvideoCamBackground()
self.hideCaptureBtns()
self.progressView.popIn()
self.progressView.transform = CGAffineTransform(rotationAngle: .pi)
})
}
})
})
}
Usage from CameraBackground Readme:
view.addCameraBackground()
// ...
view.takeCameraSnapshot( {
// animate snapshot capture
self.view.alpha = 0
UIView.animate(withDuration: 1) { self.view.alpha = 1 }
}, completion: { (capturedImage, error) -> () in
self.view.freeCameraSnapshot() // unfreeze image
// ... handle capturedImage and error
})
// ...
view.removeCameraBackground()
you can see, in usage is used this interval to set visibilty, so if you delete delay from your code, alpha stays 0. So use it how is described in GitHub with your code:
view.addCameraBackground()
view.takeCameraSnapshot( {
print("Starting...")
self.view.alpha = 0
UIView.animate(withDuration: 1) { self.view.alpha = 1 }
}, completion: { (capturedImage, error) -> () in
self.view.freeCameraSnapshot()
if capturedImage != nil {
print("compleated with image")
RZTransitionsManager.shared().defaultPresentDismissAnimationController = RZZoomAlphaAnimationController()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nextViewController = storyboard.instantiateViewController(withIdentifier: "post") as! PostViewController
nextViewController.image = capturedImage
nextViewController.transitioningDelegate = RZTransitionsManager.shared()
self.present(nextViewController, animated: true, completion: {
self.previewLiveCamera.removeCameraBackground()
self.addvideoCamBackground()
self.hideCaptureBtns()
self.progressView.popIn()
self.progressView.transform = CGAffineTransform(rotationAngle: .pi)
})
}
})
view.removeCameraBackground()

Unbalanced calls to begin/end appearance transitions with segmented control

So, I have a segmented control to switch between to view controllers.
However, often, as soon as I switch, I get this message:
Unbalanced calls to begin/end appearance transitions
Plus, after I get this, sometimes it happens that every object on the view disappear.
Here's the code:
func segmentValueChanged(sender: AnyObject) {
if sender.selectedIndex == 0 {
let newController = controllers.newController1
let oldController = childViewControllers.last as UIViewController!
self.container.frame.size.height = newController.view.frame.height
oldController.willMoveToParentViewController(nil)
addChildViewController(newController)
transitionFromViewController(oldController, toViewController: newController, duration: 0.9, options: .CurveLinear, animations:{ () -> Void in
// nothing needed here
}, completion: { (finished) -> Void in
oldController.removeFromParentViewController()
newController.didMoveToParentViewController(self)
})
} else if sender.selectedIndex == 1 {
let newController = controllers.newController2
let oldController = childViewControllers.last as UIViewController!
oldController.willMoveToParentViewController(nil)
addChildViewController(newController)
transitionFromViewController(oldController, toViewController: newController, duration: 0.9, options: .CurveLinear, animations:{ () -> Void in
// nothing needed here
}, completion: { (finished) -> Void in
oldController.removeFromParentViewController()
newController.didMoveToParentViewController(self)
})
}
}
How can I solve this?
This line is backwards:
self.container.frame.size.height = newController.view.frame.height
It is newController's view's frame that you need to set!
Moreover, you are failing to put newController.view into the interface. That is why you end up without an interface.

Passing data in transitionFromViewController

I'm using transitionFromViewController to switch between two view controllers using a segmented control.
Here's my code:
#IBAction func valueChanged(sender: AnyObject) {
var newController = storyboard?.instantiateViewControllerWithIdentifier(viewControllerIdentifiers[sender.selectedSegmentIndex]) as! UIViewController
let oldController = childViewControllers.last as! UIViewController
oldController.willMoveToParentViewController(nil)
addChildViewController(newController)
newController.view.frame = oldController.view.frame
if viewControllerIdentifiers[sender.selectedSegmentIndex] == "first" {
let vc = newController as! userProfileViewController
vc.userToShow = self.userToShow
}
transitionFromViewController(oldController, toViewController: newController, duration: 0.25, options: .TransitionCrossDissolve, animations:{ () -> Void in
// nothing needed here
}, completion: { (finished) -> Void in
oldController.removeFromParentViewController()
newController.didMoveToParentViewController(self)
})
}
is it possible to pass data to the childViewControllers, from the Parent?
Yes
transitionFromViewController(oldController, toViewController: newController, duration: 0.25, options: .TransitionCrossDissolve, animations:{ () -> Void in
newController.property = currentProperty
//basically here is where you can pass the data. I used a property as an example. You know what you need to send :)
}, completion: { (finished) -> Void in
oldController.removeFromParentViewController()
newController.didMoveToParentViewController(self)
})

transitionCoordinator return nil for custom container view controller in iOS8

when reference transitionCoordinator on child vc, it used to called transitionCoordinator on custom container class in iOS7, but in iOS8 this wasn't the case. Now it return nil and I have no clue what should I change to make this work.
I guess its about UIPresentationController introduced in iOS8, but can't find proper implementation for custom container view controller.
As Matt said in this previous SO question:
So, since you can't even get a transition coordinator in a situation
where you are allowed to write a custom transition animation for a
built-in parent view controller, obviously the chances of your getting
one in a situation where you're trying to do your own parent view
controller are zero
But, according to transitionCoordinator, overriding it is allowed:
Container view controllers can override this method but in most cases
should not need to. If you do override this method, first call super
to see if there is an appropriate transition coordinator to return,
and, if there is, return it.
So, I would try to create my own coordinator for my own VC container. If you use a UIViewPropertyAnimator to manipulate the VC container's children, it's almost straightforward.
Here is an example:
class PropertyAnimatorTransitionCoordinator: NSObject, UIViewControllerTransitionCoordinator, UIViewControllerTransitionCoordinatorContext {
private let parentView: UIView
private let fromViewController: UIViewController?
private let toViewController: UIViewController
private let animator: UIViewPropertyAnimator
// MARK: - Life Cycle
init(parentView: UIView,
fromViewController: UIViewController?,
toViewController: UIViewController,
animator: UIViewPropertyAnimator) {
self.parentView = parentView
self.fromViewController = fromViewController
self.toViewController = toViewController
self.animator = animator
}
// MARK: - UIViewControllerTransitionCoordinator
func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?,
completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool {
var isSuccessful = false
if let animation = animation {
animator.addCompletion { [weak self] _ in
guard let context = self else { return }
animation(context)
}
isSuccessful = true
}
if let completion = completion {
animator.addCompletion { [weak self] _ in
guard let context = self else { return }
completion(context)
}
isSuccessful = true
}
return isSuccessful
}
func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool {
return animate(alongsideTransition: animation, completion: completion)
}
func notifyWhenInteractionEnds(_ handler: #escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {
animator.addCompletion { [weak self] _ in
guard let context = self else { return }
handler(context)
}
}
func notifyWhenInteractionChanges(_ handler: #escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {
animator.addCompletion { [weak self] _ in
guard let context = self else { return }
handler(context)
}
}
// MARK: - UIViewControllerTransitionCoordinatorContext
var isAnimated: Bool {
return true
}
var presentationStyle: UIModalPresentationStyle {
return .none
}
var initiallyInteractive: Bool {
return false
}
var isInterruptible: Bool {
return animator.isInterruptible
}
var isInteractive: Bool {
return animator.isUserInteractionEnabled
}
var isCancelled: Bool {
return !animator.isRunning
}
var transitionDuration: TimeInterval {
return animator.duration
}
var percentComplete: CGFloat {
return animator.fractionComplete
}
var completionVelocity: CGFloat {
return 0
}
var completionCurve: UIView.AnimationCurve {
return animator.timingParameters?.cubicTimingParameters?.animationCurve ?? .linear
}
var targetTransform: CGAffineTransform {
return .identity
}
var containerView: UIView {
return parentView
}
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController? {
switch key {
case .from:
return fromViewController
case .to:
return toViewController
default:
return nil
}
}
func view(forKey key: UITransitionContextViewKey) -> UIView? {
switch key {
case .from:
return fromViewController?.view
case .to:
return toViewController.view
default:
return nil
}
}
}
In my custom container, I would use it like this:
class CustomContainerViewController: UIViewController {
private var customTransitionCoordinator: UIViewControllerTransitionCoordinator?
override var transitionCoordinator: UIViewControllerTransitionCoordinator? {
if let coordinator = super.transitionCoordinator {
return coordinator
}
return customTransitionCoordinator
}
override var shouldAutomaticallyForwardAppearanceMethods: Bool {
return false
}
func insertNewChild(_ viewController: UIViewController) {
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut)
customTransitionCoordinator = PropertyAnimatorTransitionCoordinator(
parentView: view,
fromViewController: nil,
toViewController: viewController,
animator: animator
)
animator.addCompletion { [weak self] _ in
guard let parent = self else { return }
viewController.didMove(toParent: parent)
self?.customTransitionCoordinator = nil
}
addChild(viewController)
viewController.beginAppearanceTransition(true, animated: true)
view.addSubview(viewController.view)
let target = view.bounds
viewController.view.frame = target
viewController.view.frame.origin.x = -target.width
view.layoutIfNeeded()
animator.addAnimations {
viewController.view.frame = target
}
animator.addCompletion { [weak self] _ in
guard let parent = self else { return }
viewController.endAppearanceTransition()
viewController.didMove(toParent: parent)
self?.customTransitionCoordinator = nil
}
animator.startAnimation()
}
}
Of course, some edge cases are not handled. It's a really basic example.

Resources