I'm trying to make a AVPlayerViewController go to full screen mode programmatically, coming from "embedded" mode, however this does not appear to be possible with the published API.
Is there a workaround that I'm missing? I'm interested in obtaining the same animation to the one that you get when the user presses the full screen button on the bottom right of the controls.
Using MPMoviePlayerController is not a viable alternative since I might have more than one video playing at a time.
Thanks.
AVPlayerViewController is a subclass of UIViewController, so it is presentable like any other view controller subclass. Are you able to use presentViewController:animated:completion?
self.avPlayerController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[self presentViewController:self.avPlayerController animated:YES completion:nil];
This then shows the "Done" button in the top left-hand corner.
Updated for iOS 11
There is no supported way to programmatically go fullscreen with AVPlayerViewController (a bit of an oversight in my opinion).
However, AVPlayerViewController does contain a private method that does exactly that. You'll have to decide for yourself whether you'd want to use it or not given you're not supposed to call private methods.
AVPlayerViewController+Fullscreen.h
#import <AVKit/AVKit.h>
#interface AVPlayerViewController (Fullscreen)
-(void)goFullscreen;
#end
AVPlayerViewController+Fullscreen.m
#import "AVPlayerViewController+Fullscreen.h"
#implementation AVPlayerViewController (Fullscreen)
-(void)goFullscreen {
NSString *selectorForFullscreen = #"transitionToFullScreenViewControllerAnimated:completionHandler:";
if (#available(iOS 11.3, *)) {
selectorForFullscreen = #"transitionToFullScreenAnimated:interactive:completionHandler:";
} else if (#available(iOS 11.0, *)) {
selectorForFullscreen = #"transitionToFullScreenAnimated:completionHandler:";
}
SEL fsSelector = NSSelectorFromString([#"_" stringByAppendingString:selectorForFullscreen]);
if ([self respondsToSelector:fsSelector]) {
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:fsSelector]];
[inv setSelector:fsSelector];
[inv setTarget:self];
NSInteger index = 2; //arguments 0 and 1 are self and _cmd respectively, automatically set
BOOL animated = YES;
[inv setArgument:&(animated) atIndex:index];
index++;
if (#available(iOS 11.3, *)) {
BOOL interactive = YES;
[inv setArgument:&(interactive) atIndex:index]; //arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
index++;
}
id completionBlock = nil;
[inv setArgument:&(completionBlock) atIndex:index];
[inv invoke];
}
}
#end
UPDATE: Swift 4 version of ToddH's answer:
private func enterFullscreen(playerViewController: AVPlayerViewController) {
let selectorName: String = {
if #available(iOS 11.3, *) {
return "_transitionToFullScreenAnimated:interactive:completionHandler:"
} else if #available(iOS 11, *) {
return "_transitionToFullScreenAnimated:completionHandler:"
} else {
return "_transitionToFullScreenViewControllerAnimated:completionHandler:"
}
}()
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
if playerViewController.responds(to: selectorToForceFullScreenMode) {
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
}
}
In iOS11 there are 2 new properties for AVPlayerViewController: entersFullScreenWhenPlaybackBegins and exitsFullScreenWhenPlaybackEnds. You can enable full screen mode right after playback begins and disable it when playback ends with these properties. If you need to enable fullscreen mode after some delay you can use private API methods as ToddH mentioned in his answer. However in iOS11 _transitionToFullScreenViewControllerAnimated:completionHandler: method is not available anymore, there is the same method called _transitionToFullScreenAnimated:completionHandler:. The second method accepts the same arguments as the first one.
I can show an example how to use it. First of all you need to create AVPlayerViewController instance in your UIViewController:
private let playerController : AVPlayerViewController = {
if let urlForPlayer = URL(string: "your_video_url") {
$0.player = AVPlayer(url: urlForPlayer)
}
return $0
} (AVPlayerViewController())
Then you need to setup view for AVPlayerViewController and add it to your current controller view. Function setupAVplayerController can do it for you:
private func setupAVplayerController() {
self.addChildViewController(self.playerController)
self.playerController.view.frame = CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0)
self.view.addSubview(self.playerController.view)
self.playerController.didMove(toParentViewController: self)
}
Function enterFullscreen forces full screen mode for AVPlayerViewController:
private func enterFullscreen(playerViewController:AVPlayerViewController) {
let selectorName : String = {
if #available(iOS 11, *) {
return "_transitionToFullScreenAnimated:completionHandler:"
} else {
return "_transitionToFullScreenViewControllerAnimated:completionHandler:"
}
}()
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
if playerViewController.responds(to: selectorToForceFullScreenMode) {
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
}
}
And now you need to call all these functions where you need it, for example in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//Your code
self.setupAVplayerController()
self.playerController.player?.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.enterFullscreen(playerViewController:self.playerController)
}
}
Don't forget that this solution based on private API calls that is not recommended to use.
As a little iOS 14 update to ToddH's answer: the private API to call is enterFullScreenAnimated:completionHandler:. So here's an extension on AVPlayerViewController to enter full screen.
extension AVPlayerViewController {
func enterFullScreen(animated: Bool) {
perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
func exitFullScreen(animated: Bool) {
perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
}
This implementation doesn't offer a completion callback. If you pass a Swift closure to the completionHandler parameter, it crashes in the underlying Obj-C API. I haven't investigated how to pass the closure to make it work.
Swift 3 version for the answer of ToddH:
extension AVPlayerViewController {
func goFullScreen() {
let selector = NSSelectorFromString("_transitionToFullScreenViewControllerAnimated:completionHandler:")
if self.responds(to: selector) {
// first argument is animated (true for me), second is completion handler (nil in my case)
self.perform(selector, with: true, with: nil)
}
}
}
You can just set the videoGravity property of AVPlayerViewController.
if(fullscreen)
{
[self.avPlayerController
setVideoGravity:AVLayerVideoGravityResizeAspectFill];
}
else
{
[self.avPlayerController
setVideoGravity:AVLayerVideoGravityResizeAspect];
}
For an 'embedded' AVPlayerViewController instance, it is quite easy to programmatically have it start playback in full screen mode, and without hacking anything (calling private methods). You just need to set its entersFullScreenWhenPlaybackBegins property to true.
You need to add the controller as a child VC to the main VC, and that's basically it. In viewDidAppear(_:) you need to call play() method on the controller's player property - playback will be automatically started in fullscreen.
It's often best to check Apple sample code for these kind of tricky APIs; I think this one might be useful for a lot of AVPlayer use cases: Using AVKit in iOS.
I did not have the need to use any restricted code.
For this, I am assuming that you have added the AVPlayerViewController as a child view controller.
Then for that you will first have to remove the child view controller and then present it again as a fullscreen controller as well attach the AVPlayer view properly to it's parent view.
Here is how I did it. Please note that I am using a library called Easy Peasy for restoring the playerVC.view constraints - one can do that with proper constraints as well.
#objc func fullscreenButtonClicked() {
playerVC.willMove(toParentViewController: nil)
playerVC.view.removeFromSuperview()
playerVC.removeFromParentViewController()
self.present(self.playerVC, animated: false, completion: {
self.playerVC.view.easy.layout(Top(), Right(), Left(), Bottom())
})
}
Its pretty simple, just set
playerViewController.videoGravity = .resizeAspectFill
and it goes full screen:)
Related
In my ARKit app I am presenting a modal window. When I close the modal and go back to the ARSCNView then I find out that the session is paused due to this code:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
When I close the modal and go back to the ARKit camera view screen this code gets fired:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
But this code never resumes the session. The screen is completely frozen on the last image it read. Any ideas?
I update the viewDidAppear code to be the following. It is still stuck on the camera screen with image frozen.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
sceneView.session.delegate = self
if self.isPaused {
sceneView.session.run(sceneView.session.configuration!)
} else {
// Run the view's session
sceneView.session.run(configuration)
}
}
Not sure why your session isn't resuming, but... this generally isn't a situation you want to be in anyway.
Notice in the readme that ships with Apple's ARKit sample code (attached to the WWDC17 session on ARKit):
Avoid interrupting the AR experience. If the user transitions to another fullscreen UI in your app, the AR view might not be an expected state when coming back.
Use the popover presentation (even on iPhone) for auxiliary view controllers to keep the user in the AR experience while adjusting settings or making a modal selection. In this example, the SettingsViewController and VirtualObjectSelectionViewController classes use popover presentation.
To go into a bit more detail: if you pause the session, it won't be tracking the world while your user is away in a different fullscreen view controller. That means that when you resume, any virtual content placed in the scene won't be in the positions (relative to the camera) where you left it.
I don't know if the iOS 11 GM Seed or XCode 9 GM Seed versions fixed this today however I can successfully resume a paused ARSCNview with code as in the original question.
sceneView.session.run(sceneView.session.configuration!)
I get that you have chosen an answer, and that answer is what is recommended by apple, you can restart the AR Session. You can't unpause/resume the Session though, because the device stops it's tracking once you're out of your controller presenting the ARSceneView and will stop keeping track of the position of your device relative to the objects you've placed in the scene.
Anyway, I've managed to restart the session essentially by destroying all aspects of my session and rebuilding them them when my view reappears, or through a button press.
I'll post some sample code here. It's in Objective-C cause my project was written in that, but it might help future people with the same question.
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated]
[self setupScene];
[self setupSession];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self destroySession];
[self destroyScene];
}
- (void)setupScene {
// Setup the ARSCNViewDelegate - this gives us callbacks to handle new
// geometry creation
self.sceneView.delegate = self;
// A dictionary of all the current planes being rendered in the scene
self.planes = [NSMutableDictionary new];
// Contains a list of all the boxes rendered in the scene
self.boxes = [NSMutableArray new];
// Show statistics such as fps and timing information
self.sceneView.showsStatistics = YES;
self.sceneView.autoenablesDefaultLighting = YES;
SCNScene *scene = [SCNScene new];
[self.sceneView setScene:scene];
self.sceneView.scene.physicsWorld.contactDelegate = self;
}
- (void)setupSession {
// Create a session configuration
ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfiguration new];
//ARWorldTrackingSessionConfiguration *configuration = [ARWorldTrackingSessionConfiguration new]; This has been deprecated in favor of the previous line in XCode 9 beta 5.
// Specify that we do want to track horizontal planes. Setting this will cause the ARSCNViewDelegate
// methods to be called when scenes are detected
//configuration.planeDetection = ARPlaneDetectionHorizontal;
// Run the view's session
[self.sceneView.session runWithConfiguration:configuration options:ARSessionRunOptionResetTracking];
}
-(void)destroyScene {
bottomPlane = nil;
[self.sceneView setScene:nil];
[self.sceneView setDebugOptions:nil];
self.boxes = nil;
self.planes = nil;
self.sceneView.delegate = nil;
}
-(void)destroySession {
[self.sceneView.session pause];
[self.sceneView setSession:nil];
}
These destroy methods are used when the view disappears. I am also restarting the AR Session on a button press, but it is not through these methods. It is as follows:
-(void)resetPressed{
NSLog(#"Reset Pressed");
[_sceneView.session pause];
SCNScene *scene = [[SCNScene alloc] init];
[_sceneView setScene:scene];
[_sceneView.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) {
[child removeFromParentNode];
}];
ARWorldTrackingConfiguration *configuration = [[ARWorldTrackingSessionConfiguration ARWorldTrackingConfiguration] init];
[_sceneView.session runWithConfiguration:configuration options:ARSessionRunOptionResetTracking | ARSessionRunOptionRemoveExistingAnchors];
}
Hope it helps.
Here's an answer working with Swift 4.2 and iOS 12.
To present UI defined in another view controller over your AR scene, create your view controller instance and set it's modalPresentationStyle property to .overCurrentContext:
EXAMPLE:
func showMaterialPicker(completion: (Texture?) -> Void) {
// create an instance of your view controller, I have convenience functions
// setup to do this via an extension on UIViewController
guard let materialPicker = MaterialCategoriesViewController.instance(from: .product) else {
print("Unable to instantiate MaterialCategoriesViewController, bailing")
return
}
// set presentation style and transition style
materialPicker.modalPresentationStyle = .overCurrentContext
materialPicker.modalTransitionStyle = .crossDissolve
// present the controller
present(materialPicker, animated: true, completion: nil)
}
Bonus tip:
To make your overlay appear to slide up from the bottom like a drawer, set
materialPicker.modalTransitionStyle = .coverVertical
then constrain your views in your overlay view controller a comfortable height from the bottom and set the background color of the view controllers view to UIColor.clear.
If you want to darken the AR view while your overlay is displayed you can set the background color to a black color with an opacity/alpha value of approximately 0.75.
Something like this:
self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.75)
or in the storyboard:
In the screenshot above I have a tableview pinned to the bottom and sides of the overlay view controllers view, and a height constraint of 300.
When done this way you can still see the AR view behind the overlay view controller and the scene continues to render.
Inside viewDidLoad: create a tap event.
arKitView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))))
Inside viewWillAppear:
arView.session.run(configuration)
And then define the tap event. In which first tap will pause the session and second tap will resume the session and so on.
Some times in my app I get this error because the UI freezes and the users tap more than once the buttons:
"pushing the same view controller instance more than once is not
supported"
I have tried this:
How to prevent multiple event on same UIButton in iOS?
And it works like a charm but if my tabbar has more than 5 elements if I tab the button that shows an element greater than 5 the more button animates from left to right.
Is there other way to prevent the double tab in an easy way that does not use animations?.
This is the code I'm using:
- (IBAction)btnAction:(id)sender {
UIButton *bCustom = (UIButton *)sender;
bCustom.userInteractionEnabled = NO;
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent animations:^{
[self selectTabControllerIndex:bCustom.tag];
} completion:^(BOOL finished){
bCustom.userInteractionEnabled = YES;
}];
}
First a tip, if you only have button's calling that selector, you can change the id to UIButton* and drop the extra variable bCustom.
Now, to solve your issue, you just need to ensure you turn userInteractionEnabled back to YES after you'd done whatever else you needed to do. Using the animation block is just an easy way because it has a completion handler built in.
You can do this simply by having selectTabControllerIndex method do the work for you.
Something like this:
- (IBAction)btnAction:(UIButton*)sender {
sender.userInteractionEnabled = NO;
[self selectTabControllerForButton:sender];
}
- (void)selectTabControllerForButton:(UIButton*)sender {
// Whatever selectTabControllerIndex does now goes here, use sender.tag as you used index
sender.userInteractionEnabled = YES;
}
If you possibly had other code you needed to execute afterwards, you could add a completion handler to your selectTabControllerIndex method instead and then call the completion handler. Inside that you'd include the sender.userInteractionEnabled = YES; line. But if it's always the same code, the first way is easier and faster.
Using userInteractionEnable=false to prevent double tap is like using a Rocket Launcher to kill a bee.
Instead, you can use myButton.enabled=false.Using this, you may be able to change ( if you want ) the layout of your button when it is deactivated.
In Swift, you can also use defer keyword, to execute a block of code that will be executed only when execution leaves the current scope.
#IBAction func btnAction(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
defer {
sender.isUserInteractionEnabled = true
}
// rest of your code goes here
}
Note: This will only be helpful if the "rest of your code" is not async, so that the execution actually leaves the current scope.
In async cases you'd need to set isUserInteractionEnabled = true at the end of that async method.
Disable isUserInteractionEnabled or disable the button not work some cases, if have background API calling in next controller, push process will work asynchronously.
After some work around i thought its better to go with the other way, i found Completion handler in Objective-C or Closure in Swift can be good here.
Here is the example which i used in Objective c:
-(void)didSettingClick:(id) sender
{
if (!isPushInProcess) {
isPushInProcess = YES;
SettingVC *settings = [[SettingVC alloc] initWithcomplition:^{
isPushInProcess = NO;
}];
[self.navigationController pushViewController:settings animated:YES];
}
}
Here is method description:
dispatch_block_t pushComplition;
-(instancetype) initWithcomplition:(dispatch_block_t)complition{
self = [super init];
if (self) {
pushComplition = complition;
}
return self;
}
Inside viewDidAppear()
-(void)viewDidAppear:(BOOL)animated
{
pushComplition();
}
In swift using defer keyword is also can be good idea.
Hope It help!!!
You can disable the userInteraction for that button when user taps for first time.
Then new view controller will appear, while leaving to new View Controller call this
-(IBAction)btnAction:(UIButton *)sender {
sender.userInteractionEnabled=NO;
//do your code
}
if it is moving to another view then call below one
-(void)viewWillDisappear {
buttonName.userInteractionEnabled=YES;
}
if not moving from present view
you can call
sender.userInteractionEnabled=YES;
at the end of btnAction method.
It will work for sure.
myButton.multipleTouchEnabled = NO;
Swift 4 version of #Santo answer that worked for me:
Button code:
#IBAction func btnMapTap(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
//put here your code
Add override method viewWillDisappear:
override func viewWillDisappear(_ animated: Bool) {
btnMap.isUserInteractionEnabled = true
}
Use this code: This is bool condition
button.ismultipleTouchEnabled = false
it seems that under iOS 14.x it will happen automatically when You tap.
I have written small demo app with a nav controller, a controller of class "ViewController" with a button invoking an action "pushIt".
(see code)
I have set Storyboard ID to a separated controller to "ColoredVCID" and added a global counter, just to see...
Long way SHORT: it seems working correctly.
// compulsiveTouch
//
// Created by ing.conti on 03/08/21.
import UIKit
fileprivate var cont = 0
class ViewController: UIViewController {
#IBAction func pushIt(_ sender: Any) {
cont+=1
print(cont)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ColoredVCID")
self.navigationController!.present(vc, animated: true)
// OR:
self.navigationController!.pushViewController(vc, animated: true)
}
}
In PAST days I usually did:
#objc func pushItOLD(_sender: Any){
// prevent compulsive touch:
self.setButtonActive(btn: self.pushBtn!, active: false)
// now re-eanble it... after 1 second:
let when = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: when, execute: { () -> Void in
self.setButtonActive(btn: self.pushBtn!, active: true)
})
}
func setButtonActive(btn: UIButton?, active: Bool){
guard let btn = btn else{
return
}
btn.isEnabled = active
btn.alpha = (active ? 1 : 0.5)
}
that CAN BE very useful nowadays if your button for example invokes a network request... to prevent double calls.
(I added some cosmetics to use alpha.. to let user see it as "disabled" ..)
I did it like this
var callInProgress = false
func call(){
if callInProgress == true{
return
}
callInProgress = true
//Make it false when your task done
}
it will not allow user to call the function one more time untill you make callInProgress false
This is the only thing working
I currently have a UIView that programmatically has an embedded QLPreviewController in it. I need to get rid of the default navigator bar that the QLPreviewController has when the document/url is loaded. Is there a way to do this?
Currently, I've tried subclassing QLPreviewController and in the viewDidAppear set self.navigationController!.navigationBarHidden = true. But this doesn't work.
Sorry if this is a dupe question - I've been looking online the last few days and couldn't find a concrete answer with iOS 8/9.
I Solve this problem by using addChildViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupPreviewController];
}
- (void)setupPreviewController {
self.previewController = [[QLPreviewController alloc] init];
[self addChildViewController:self.previewController];
[self.view addSubview:self.previewController.view];
//do autolayout
[self.previewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.right.bottom.equalTo(self.view);
}];
self.navigationController.navigationBarHidden = YES;
}
Same thing apply in viewWillAppear and in viewDidLoad methods
self.navigationController!.navigationBarHidden = true
i hope this will help
It does appear to be possible. After inspecting the view hierarchy at runtime I found that the nav bar you see is actually a subview of the View Controller's view. The code below will remove it; however, it will not stay gone and it does not appear that there is any sanctioned way to modify the UI elements of this class. Any modification of this class will be a fragile hack and I'd recommend finding something less locked down to customize.
class MyPreviewViewController: QLPreviewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let subviewsWithNav = self.view.subviews.first?.subviews {
for view in subviewsWithNav {
if let navbar = view as? UINavigationBar {
navbar.isHidden = true
}
}
}
}
}
This worked for me:
class CustomQLPreview: QLPreviewController {
...
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navigationController?.setNavigationBarHidden(true, animated: false)
}
}
When my child performs an unwind segue, my controller's viewDidAppear gets called.
In this method (and this method alone, I need to know whether it was from an unwind or not)
Note: the child is unwinding to the very first view controller, so this is an intermediate view controller, not the true root.
You should be able to use the following to detect in each controller if the exposure of the view controller was as a result of being pushed/presented, or as a result of being exposed as a result of pop/dismiss/unwind.
This may or may be enough for your needs.
- (void) viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Handle controller being exposed from push/present or pop/dismiss
if (self.isMovingToParentViewController || self.isBeingPresented){
// Controller is being pushed on or presented.
}
else{
// Controller is being shown as result of pop/dismiss/unwind.
}
}
If you want to know that viewDidAppear was called because of an unwind segue as being different from a conventional pop/dismiss being called, then you need to add some code to detect that an unwind happened. To do this you could do the following:
For any intermediate controller you want to detect purely an unwind in, add a property of the form:
/** BOOL property which when TRUE indicates an unwind occured. */
#property BOOL unwindSeguePerformed;
Then override the unwind segue method canPerformUnwindSegueAction:fromViewController:withSender: method as follows:
- (BOOL)canPerformUnwindSegueAction:(SEL)action
fromViewController:(UIViewController *)fromViewController
withSender:(id)sender{
// Set the flag indicating an unwind segue was requested and then return
// that we are not interested in performing the unwind action.
self.unwindSeguePerformed = TRUE;
// We are not interested in performing it, so return NO. The system will
// then continue to look backwards through the view controllers for the
// controller that will handle it.
return NO;
}
Now you have a flag to detect an unwind and a means to detect the unwind just before it happens. Then adjust the viewDidAppear method to include this flag.
- (void) viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Handle controller being exposed from push/present or pop/dismiss
// or an unwind
if (self.isMovingToParentViewController || self.isBeingPresented){
// Controller is being pushed on or presented.
// Initialize the unwind segue tracking flag.
self.unwindSeguePerformed = FALSE;
}
else if (self.unwindSeguePerformed){
// Controller is being shown as a result of an unwind segue
}
else{
// Controller is being shown as result of pop/dismiss.
}
}
Hopefully this meets your requirement.
For docs on handling the unwind segue chain see: https://developer.apple.com/library/ios/technotes/tn2298/_index.html
Here is a simple category on UIViewController that you can use to track whether your presented view controller is in the midst of an unwind segue. I suppose it could be flushed out more but I believe this much works for your case.
To use it you need to register the unwind segue from your unwind action method on the destination view controller:
- (IBAction) prepareForUnwind:(UIStoryboardSegue *)segue
{
[self ts_registerUnwindSegue: segue];
}
That's it. From your intermediate view controller, you can test if you are in the midst of an unwind segue:
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear: animated];
BOOL unwinding = [self ts_isUnwinding];
NSLog( #"%#:%#, unwinding: %#", self.title, NSStringFromSelector(_cmd), unwinding ? #"YES" : #"NO" );
}
There's no need to clean anything up; the segue will self-deregister when it ends.
Here's the full category:
#interface UIViewController (unwinding)
- (void) ts_registerUnwindSegue: (UIStoryboardSegue*) segue;
- (BOOL) ts_isUnwinding;
#end
static NSMapTable* g_viewControllerSegues;
#implementation UIViewController (unwinding)
- (void) ts_registerUnwindSegue: (UIStoryboardSegue*) segue
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
g_viewControllerSegues = [NSMapTable weakToWeakObjectsMapTable];
});
for ( UIViewController* vc = segue.sourceViewController ; vc != nil ; vc = vc.presentingViewController )
{
[g_viewControllerSegues setObject: segue forKey: vc];
}
}
- (BOOL) ts_isUnwinding
{
return [g_viewControllerSegues objectForKey: [self ts_topMostParentViewController]] != nil;
}
- (UIViewController *)ts_topMostParentViewController {
UIViewController *viewController = self;
while (viewController.parentViewController) {
viewController = viewController.parentViewController;
}
return viewController;
}
#end
Your question was really interesting to me, because I never used IB and segues before (don't judge me for that) and wanted to learn something new. As you described in your comments:
viewDidAppear will be called on B when C rewinds to A
So I come up with an easy custom solution to this:
protocol ViewControllerSingletonDelegate: class {
func viewControllerWillUnwind(viewcontroller: UIViewController, toViewController: UIViewController)
}
class ViewControllerSingleton {
static let sharedInstance = ViewControllerSingleton()
private var delegates: [ViewControllerSingletonDelegate] = []
func addDelegate(delegate: ViewControllerSingletonDelegate) {
if !self.containsDelegate(delegate) {
self.delegates.append(delegate)
}
}
func removeDelegate(delegate: ViewControllerSingletonDelegate) {
/* implement any other function by your self :) */
}
func containsDelegate(delegate: ViewControllerSingletonDelegate) -> Bool {
for aDelegate in self.delegates {
if aDelegate === delegate { return true }
}
return false
}
func forwardToDelegate(closure: (delegate: ViewControllerSingletonDelegate) -> Void) {
for aDelegate in self.delegates { closure(delegate: aDelegate) }
}
}
class SomeViewController: UIViewController, ViewControllerSingletonDelegate {
let viewControllerSingleton = ViewControllerSingleton.sharedInstance
func someFunction() { // some function where you'll set the delegate
self.viewControllerSingleton.addDelegate(self)
}
/* I assume you have something like this in your code */
#IBAction func unwindToSomeOtherController(unwindSegue: UIStoryboardSegue) {
self.viewControllerSingleton.forwardToDelegate { (delegate) -> Void in
delegate.viewControllerWillUnwind(unwindSegue.sourceViewController, toViewController: unwindSegue.destinationViewController)
}
/* do something here */
}
// MARK: - ViewControllerSingletonDelegate
func viewControllerWillUnwind(viewcontroller: UIViewController, toViewController: UIViewController) {
/* do something with the callback */
/* set some flag for example inside your view controller so your viewDidAppear will know what to do */
}
}
You also could modify the callback function to return something else, like controller identifier instead the controller itself.
I do everything programmatically, so please don't judge me for that too. ;)
If this code snippet won't help you, I'd still love to see some feedback.
Suppose the segue navigation is ViewController -> FirstViewController -> SecondViewController. There is an unwind from SecondViewController to ViewController. You can add in the intermediary FirstViewController the following code to detect unwind actions.
import UIKit
class FirstViewController: UIViewController {
var unwindAction:Bool = false
override func viewDidAppear(animated: Bool) {
if unwindAction {
println("Unwind action")
unwindAction = false
}
}
override func viewControllerForUnwindSegueAction(action: Selector, fromViewController: UIViewController, withSender sender: AnyObject?) -> UIViewController? {
self.unwindAction = true
return super.viewControllerForUnwindSegueAction(action, fromViewController: fromViewController, withSender: sender)
}
}
EDIT
After giving this some thought, I decided the solution to this depends on the kind of complexity that you are dealing with here. What exactly do you do when you do the unwind segue? The solutions given here are viable and they work -- only if you want to detect whether it is an unwind action. What if you want to pass the data between the point where the unwind is happening to the root? What if there is a complex set of preparations that you wanna do in one of the intermediate view controllers? What if you want to do both of these?
In such complex scenarios, I would immediately rule out overriding the unwind methods of the view controller. Doing such operations there will work, but it won't be clean. A method will be doing what it isn't supposed to do. Smell that? That's code smell.
What if, somehow a view controller could inform the next view controller in the hierarchy of the event happening? Better yet, how do we do this without tightly coupling these two?
Protocol.
Have a protocol definition something like:
protocol UnwindResponding {
prepareForUnwindSegue(segue:UISegue , formViewController:UIViewController, withImportantInfo info:[String,AnyObject])
}
Using protocol you will keep the relationship between the objects -- the hierarchy of view controllers in this case -- explicit. At the point of occurrence of a particular event, you will delegate the call to the next controller in the hierarchy informing about the happening of a particular event in another view controller. Here is an example:
override func prepareForSegue(segue:UIStoryboardSegue, sender:AnyObject?) {
if let unwindResponder = self.presentingViewController as? UnwindResponding where segue.identifier = "unwindSegue" {
unwindResponder.prepareForUnwindSegue(segue:UISegue, fromViewController:self,info:info)
}
}
In the intermediary view controller you can do something like:
extension IntermediaryViewController : UnwindResponding {
prepareForUnwindSegue(segue:UISegue , fromViewController:UIViewController, withImportantInfo info:[String,AnyObject]) {
if let unwindResponder = self.presentingViewController {
unwindResponder.prepareForUnwindSegue(segue,fromViewController:fromViewController, info:info)
}
unwindSegue = true
}
}
Granted, you wouldn't wanna do this if you just want to detect unwind segues. Maybe you do, you'll never know what will happen in the future. Never hurts to keep your code clean.
Add method in your parent view controller
#IBAction func unwindToParent(unwindSegue: UIStoryboardSegue) {
if let childViewController = unwindSegue.sourceViewController as? ChildViewController {
println("unwinding from child")
}
}
As an exemple if the unwind segue is related to a button, in the storyboard link your button to it's view controller exit
It will propose to link to unwindToParent method
Then each time the unwind segue is performed, the unwindToParent method will be called
You can override the function unwindForSegue:towardsViewController:, which is called when the ViewController is on the path of an unwind segue. It's meant to be used to reconfigure the ViewController.
Swift example:
override func unwind(for unwindSegue: UIStoryboardSegue, towardsViewController subsequentVC: UIViewController) {
}
Is there a built-in way to get from a UIView to its UIViewController? I know you can get from UIViewController to its UIView via [self view] but I was wondering if there is a reverse reference?
Using the example posted by Brock, I modified it so that it is a category of UIView instead UIViewController and made it recursive so that any subview can (hopefully) find the parent UIViewController.
#interface UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController;
#end
#implementation UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController {
UIResponder *responder = [self nextResponder];
while (responder != nil) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController *)responder;
}
responder = [responder nextResponder];
}
return nil;
}
#end
To use this code, add it into an new class file (I named mine "UIKitCategories") and remove the class data... copy the #interface into the header, and the #implementation into the .m file. Then in your project, #import "UIKitCategories.h" and use within the UIView code:
// from a UIView subclass... returns nil if UIViewController not available
UIViewController * myController = [self firstAvailableUIViewController];
UIView is a subclass of UIResponder. UIResponder lays out the method -nextResponder with an implementation that returns nil. UIView overrides this method, as documented in UIResponder (for some reason instead of in UIView) as follows: if the view has a view controller, it is returned by -nextResponder. If there is no view controller, the method will return the superview.
Add this to your project and you're ready to roll.
#interface UIView (APIFix)
- (UIViewController *)viewController;
#end
#implementation UIView (APIFix)
- (UIViewController *)viewController {
if ([self.nextResponder isKindOfClass:UIViewController.class])
return (UIViewController *)self.nextResponder;
else
return nil;
}
#end
Now UIView has a working method for returning the view controller.
Since this has been the accepted answer for a long time, I feel I need to rectify it with a better answer.
Some comments on the need:
Your view should not need to access the view controller directly.
The view should instead be independent of the view controller, and be able to work in different contexts.
Should you need the view to interface in a way with the view controller, the recommended way, and what Apple does across Cocoa is to use the delegate pattern.
An example of how to implement it follows:
#protocol MyViewDelegate < NSObject >
- (void)viewActionHappened;
#end
#interface MyView : UIView
#property (nonatomic, assign) MyViewDelegate delegate;
#end
#interface MyViewController < MyViewDelegate >
#end
The view interfaces with its delegate (as UITableView does, for instance) and it doesn't care if its implemented in the view controller or in any other class that you end up using.
My original answer follows: I don't recommend this, neither the rest of the answers where direct access to the view controller is achieved
There is no built-in way to do it. While you can get around it by adding a IBOutlet on the UIView and connecting these in Interface Builder, this is not recommended. The view should not know about the view controller. Instead, you should do as #Phil M suggests and create a protocol to be used as the delegate.
I would suggest a more lightweight approach for traversing the complete responder chain without having to add a category on UIView:
#implementation MyUIViewSubclass
- (UIViewController *)viewController {
UIResponder *responder = self;
while (![responder isKindOfClass:[UIViewController class]]) {
responder = [responder nextResponder];
if (nil == responder) {
break;
}
}
return (UIViewController *)responder;
}
#end
Combining several already given answers, I'm shipping on it as well with my implementation:
#implementation UIView (AppNameAdditions)
- (UIViewController *)appName_viewController {
/// Finds the view's view controller.
// Take the view controller class object here and avoid sending the same message iteratively unnecessarily.
Class vcc = [UIViewController class];
// Traverse responder chain. Return first found view controller, which will be the view's view controller.
UIResponder *responder = self;
while ((responder = [responder nextResponder]))
if ([responder isKindOfClass: vcc])
return (UIViewController *)responder;
// If the view controller isn't found, return nil.
return nil;
}
#end
The category is part of my ARC-enabled static library that I ship on every application I create. It's been tested several times and I didn't find any problems or leaks.
P.S.: You don't need to use a category like I did if the concerned view is a subclass of yours. In the latter case, just put the method in your subclass and you're good to go.
I modified de answer so I can pass any view, button, label etc. to get it's parent UIViewController. Here is my code.
+(UIViewController *)viewController:(id)view {
UIResponder *responder = view;
while (![responder isKindOfClass:[UIViewController class]]) {
responder = [responder nextResponder];
if (nil == responder) {
break;
}
}
return (UIViewController *)responder;
}
Edit Swift 3 Version
class func viewController(_ view: UIView) -> UIViewController {
var responder: UIResponder? = view
while !(responder is UIViewController) {
responder = responder?.next
if nil == responder {
break
}
}
return (responder as? UIViewController)!
}
Edit 2:- Swift Extention
extension UIView
{
//Get Parent View Controller from any view
func parentViewController() -> UIViewController {
var responder: UIResponder? = self
while !(responder is UIViewController) {
responder = responder?.next
if nil == responder {
break
}
}
return (responder as? UIViewController)!
}
}
Even though this can technically be solved as pgb recommends, IMHO, this is a design flaw. The view should not need to be aware of the controller.
Don't forget that you can get access to the root view controller for the window that the view is a subview of. From there, if you are e.g. using a navigation view controller and want to push a new view onto it:
[[[[self window] rootViewController] navigationController] pushViewController:newController animated:YES];
You will need to set up the rootViewController property of the window properly first, however. Do this when you first create the controller e.g. in your app delegate:
-(void) applicationDidFinishLaunching:(UIApplication *)application {
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
RootViewController *controller = [[YourRootViewController] alloc] init];
[window setRootViewController: controller];
navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
[controller release];
[window addSubview:[[self navigationController] view]];
[window makeKeyAndVisible];
}
I stumbled upon a situation where I have a small component I want to reuse, and added some code in a reusable view itself(it's really not much more than a button that opens a PopoverController).
While this works fine in the iPad (the UIPopoverController presents itself, therefor needs no reference to a UIViewController), getting the same code to work means suddenly referencing your presentViewController from your UIViewController. Kinda inconsistent right?
Like mentioned before, it's not the best approach to have logic in your UIView. But it felt really useless to wrap the few lines of code needed in a separate controller.
Either way, here's a swift solution, which adds a new property to any UIView:
extension UIView {
var viewController: UIViewController? {
var responder: UIResponder? = self
while responder != nil {
if let responder = responder as? UIViewController {
return responder
}
responder = responder?.nextResponder()
}
return nil
}
}
While these answers are technically correct, including Ushox, I think the approved way is to implement a new protocol or re-use an existing one. A protocol insulates the observer from the observed, sort of like putting a mail slot in between them. In effect, that is what Gabriel does via the pushViewController method invocation; the view "knows" that it is proper protocol to politely ask your navigationController to push a view, since the viewController conforms to the navigationController protocol. While you can create your own protocol, just using Gabriel's example and re-using the UINavigationController protocol is just fine.
I don't think it's "bad" idea to find out who is the view controller for some cases. What could be a bad idea is to save the reference to this controller as it could change just as superviews change.
In my case I have a getter that traverses the responder chain.
//.h
#property (nonatomic, readonly) UIViewController * viewController;
//.m
- (UIViewController *)viewController
{
for (UIResponder * nextResponder = self.nextResponder;
nextResponder;
nextResponder = nextResponder.nextResponder)
{
if ([nextResponder isKindOfClass:[UIViewController class]])
return (UIViewController *)nextResponder;
}
// Not found
NSLog(#"%# doesn't seem to have a viewController". self);
return nil;
}
Swift 4
(more concise than the other answers)
fileprivate extension UIView {
var firstViewController: UIViewController? {
let firstViewController = sequence(first: self, next: { $0.next }).first(where: { $0 is UIViewController })
return firstViewController as? UIViewController
}
}
My use case for which I need to access the view first UIViewController: I have an object that wraps around AVPlayer / AVPlayerViewController and I want to provide a simple show(in view: UIView) method that will embed AVPlayerViewController into view. For that, I need to access view's UIViewController.
Two solutions as of Swift 5.2:
More on the functional side
No need for the return keyword now 🤓
Solution 1:
extension UIView {
var parentViewController: UIViewController? {
sequence(first: self) { $0.next }
.first(where: { $0 is UIViewController })
.flatMap { $0 as? UIViewController }
}
}
Solution 2:
extension UIView {
var parentViewController: UIViewController? {
sequence(first: self) { $0.next }
.compactMap{ $0 as? UIViewController }
.first
}
}
This solution requires iterating through each responder first, so may not be the most performant.
The simplest do while loop for finding the viewController.
-(UIViewController*)viewController
{
UIResponder *nextResponder = self;
do
{
nextResponder = [nextResponder nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]])
return (UIViewController*)nextResponder;
} while (nextResponder != nil);
return nil;
}
This doesn't answer the question directly, but rather makes an assumption about the intent of the question.
If you have a view and in that view you need to call a method on another object, like say the view controller, you can use the NSNotificationCenter instead.
First create your notification string in a header file
#define SLCopyStringNotification #"ShaoloCopyStringNotification"
In your view call postNotificationName:
- (IBAction) copyString:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:SLCopyStringNotification object:nil];
}
Then in your view controller you add an observer. I do this in viewDidLoad
- (void)viewDidLoad
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(copyString:)
name:SLCopyStringNotification
object:nil];
}
Now (also in the same view controller) implement your method copyString: as depicted in the #selector above.
- (IBAction) copyString:(id)sender
{
CalculatorResult* result = (CalculatorResult*)[[PercentCalculator sharedInstance].arrayTableDS objectAtIndex:([self.viewTableResults indexPathForSelectedRow].row)];
UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
[gpBoard setString:result.stringResult];
}
I'm not saying this is the right way to do this, it just seems cleaner than running up the first responder chain. I used this code to implement a UIMenuController on a UITableView and pass the event back up to the UIViewController so I can do something with the data.
It's surely a bad idea and a wrong design, but I'm sure we can all enjoy a Swift solution of the best answer proposed by #Phil_M:
static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
if let nextResponder = responder.nextResponder() {
if let nextResp = nextResponder as? UIViewController {
return nextResp
} else {
return traverseResponderChainForUIViewController(nextResponder)
}
}
return nil
}
return traverseResponderChainForUIViewController(responder)
}
If your intention is to do simple things, as showing a modal dialog or tracking data, that doesn't justify the use of a protocol. I personally store this function in an utility object, you can use it from anything that implement the UIResponder protocol as:
if let viewController = MyUtilityClass.firstAvailableUIViewController(self) {}
All credit to #Phil_M
Maybe I'm late here. But in this situation I don't like category (pollution). I love this way:
#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})
Swiftier solution
extension UIView {
var parentViewController: UIViewController? {
for responder in sequence(first: self, next: { $0.next }) {
if let viewController = responder as? UIViewController {
return viewController
}
}
return nil
}
}
Swift 4 version
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
Usage example
if let parent = self.view.parentViewController{
}
Updated version for swift 4 : Thanks for #Phil_M and #paul-slm
static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
if let nextResponder = responder.next {
if let nextResp = nextResponder as? UIViewController {
return nextResp
} else {
return traverseResponderChainForUIViewController(responder: nextResponder)
}
}
return nil
}
return traverseResponderChainForUIViewController(responder: responder)
}
To Phil's answer:
In line: id nextResponder = [self nextResponder]; if self(UIView) is not a subview of ViewController's view, if you know hierarchy of self(UIView) you can use also: id nextResponder = [[self superview] nextResponder];...
If you aren't going to upload this to the App Store, you can also use a private method of UIView.
#interface UIView(Private)
- (UIViewController *)_viewControllerForAncestor;
#end
// Later in the code
UIViewController *vc = [myView _viewControllerForAncestor];
var parentViewController: UIViewController? {
let s = sequence(first: self) { $0.next }
return s.compactMap { $0 as? UIViewController }.first
}
My solution would probably be considered kind of bogus but I had a similar situation as mayoneez (I wanted to switch views in response to a gesture in an EAGLView), and I got the EAGL's view controller this way:
EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;
I think there is a case when the observed needs to inform the observer.
I see a similar problem where the UIView in a UIViewController is responding to a situation and it needs to first tell its parent view controller to hide the back button and then upon completion tell the parent view controller that it needs to pop itself off the stack.
I have been trying this with delegates with no success.
I don't understand why this should be a bad idea?
Another easy way is to have your own view class and add a property of the view controller in the view class. Usually the view controller creates the view and that is where the controller can set itself to the property. Basically it is instead of searching around (with a bit of hacking) for the controller, having the controller to set itself to the view - this is simple but makes sense because it is the controller that "controls" the view.
To get the controller of a given view, one can use UIFirstResponder chain.
customView.target(forAction: Selector("viewDidLoad"), withSender: nil)
If your rootViewController is UINavigationViewController, which was set up in AppDelegate class, then
+ (UIViewController *) getNearestViewController:(Class) c {
NSArray *arrVc = [[[[UIApplication sharedApplication] keyWindow] rootViewController] childViewControllers];
for (UIViewController *v in arrVc)
{
if ([v isKindOfClass:c])
{
return v;
}
}
return nil;}
Where c required view controllers class.
USAGE:
RequiredViewController* rvc = [Utilities getNearestViewController:[RequiredViewController class]];
There is no way.
What I do is pass the UIViewController pointer to the UIView (or an appropriate inheritance). I'm sorry I can't help with the IB approach to the problem because I don't believe in IB.
To answer the first commenter: sometimes you do need to know who called you because it determines what you can do. For example with a database you might have read access only or read/write ...