I am having a really hard time getting this to work. I have two view controllers with associated views called DomainSelectionViewController and DomainViewController. I'm going through a tutorial on Apple's developer network that covers presenting view controllers. I'm getting an EXC_BAD_ACCESS signal when trying to run.
Here are the relevant excerpts from each file:
DomainSelectionViewController.h
#class DomainViewController;
#interface DomainSelectionViewController : UIViewController
- (IBAction)domainSelected:(id)sender;
- (IBAction)leaveDomain;
#property (retain) DomainViewController * selectedDomain;
#end
domainSelected: is attached to a button that represents a domain. Clicking on it successfully replaces the current view in the interface with the view defined in DomainViewController's nib.
DomainSelectionViewController.m
#implementation
- (IBAction)domainSelected:(id)sender {
NSLog(#"Domain Selected...");
selectedDomain = [[DomainViewController alloc] initWithNibName:#"DomainView" bundle:nil];
selectedDomain.domainSelectionContext = self;
[self presentViewController:selectedDomain animated:NO completion:nil];
}
- (IBAction)leaveDomain {
NSLog(#"Leaving Domain...");
NSLog(#"Presented Domain: %#", self.presentedViewController);
//selectedDomain.modalPresentationStyle = UIModalPresentationFullScreen;
[self dismissViewControllerAnimated:NO completion:nil];
}
DomainViewController.h
#import <UIKit/UIKit.h>
#import "DomainSelectionViewController.h"
#class DomainSelectionViewController;
#interface DomainViewController : UIViewController
//#property (nonatomic, assign) DomainSelectionViewController * presentingViewController;
#property (nonatomic, retain) DomainSelectionViewController * domainSelectionContext;
#end
DomainViewController.m
- (IBAction)exit:(id)sender {
NSLog(#"Leaving Domain...");
if(self.presentingViewController) {
NSLog(#" Dismissing View Controller: %#.", self.presentingViewController);
[self.domainSelectionContext leaveDomain];
//[self.presentingViewController dismissViewControllerAnimated:NO completion:nil];
return;
}
else {
NSLog(#"Presenting view controller not set.");
}
}
The domainview contains only one button that reads "back" and is connected to its own exit: function, which in turn calls leaveDomain on its delegate. It is on clicking on this button that the EXC_BAD_ACCESS call arises. Looking at other similar posts, it's said that the EXC_BAD_ACCESS error typically arises from trying to call upon a deallocated object, but the print statement just before the dismiss call shows that the objects are still there and can be referred to. I was hoping someone with more experience than I could look at this and easily divine what has gone wrong.
For completeness's sake, here's the output from the console:
Attaching to process 26860.
2012-03-24 19:23:45.601 domaintest[26860:f803] DomainSelectionView Initialized.
2012-03-24 19:23:52.627 domaintest[26860:f803] Domain Selected...
2012-03-24 19:24:14.187 domaintest[26860:f803] Leaving Domain...
2012-03-24 19:24:14.188 domaintest[26860:f803] Dismissing View Controller: <DomainSelectionViewController: 0x688f9a0>.
2012-03-24 19:24:14.188 domaintest[26860:f803] Leaving Domain...
2012-03-24 19:24:14.188 domaintest[26860:f803] Presented Domain: <DomainViewController: 0x6891d90>
Current language: auto; currently objective-c
warning: Attempting to create USE_BLOCK_IN_FRAME variable with block that isn't in the frame.
(gdb)
So after quite a bit of trial-and-error, the issue here was higher up in the view hierarchy. The design had a root view controller that was completely empty, which replaced its own view with the first (DomainSelectionViewController) controller's view
self.window.rootViewController.view = domainSelectionViewController.view;
The end result of which is that domainSelectionViewController could present domainViewController's view, but trying to dismiss it resulted in EXC_BAD_ACCESS. I'm still not entirely sure why, but changing it such that domainSelectionViewController was the primary view, or having rootViewController present domainSelectionViewController in ViewDidAppear fixed the issue.
Oh, well there's your problem.
You are calling the function:
[self dismissViewControllerAnimated:NO completion:nil];
This is the function that should be incorporated in the new view controller that you are trying to display, not in the original view, as you are trying to destroy the root view.
Instead, do this:
DomainSelectionViewController.m
- (IBAction)leaveDomain {
NSLog(#"Leaving Domain...");
NSLog(#"Presented Domain: %#", self.presentedViewController);
//selectedDomain.modalPresentationStyle = UIModalPresentationFullScreen;
}
DomainViewController.m
- (IBAction)exit:(id)sender {
NSLog(#"Leaving Domain...");
if(self.presentingViewController) {
NSLog(#" Dismissing View Controller: %#.", self.presentingViewController);
[self.domainSelectionContext leaveDomain];
//ADD THIS HERE
[self dismissViewControllerAnimated:NO completion:nil];
return;
}
else {
NSLog(#"Presenting view controller not set.");
}
}
This way, you get rid of the view controller you are presenting. Using self.presentingViewController will not work either because the parent view controller is the one you want to dismiss in order to see the selection view beneath it; dismissing the presented controller will leave you with a white screen.
Hope this helps you and good luck!
Related
I've got two UIViewControllers. I'm using modal segue to the second one, when coming back I use dismissViewControllerAnimated. I want to fire a method when I come back to the first one. How can I do that?
I tried to fire a custom notification before dismissViewControllerAnimated and catching it in the first UIViewController, but it doesn't catch anything, because it's still on the second one when it's fired.
There are easy options I can see.
Use the viewDidDisappear: method in the view you're dismissing.
dismissViewControllerAnimated:completion: method accepts a block that actually executes after viewDidDisappear executes in the dismissing view.
To pass a reference from one view controller to the next:
In the second view controller's .h file, add a property:
#property (nonatomic,strong) FirstViewController *firstVC;
In your first view controller, add the following method:
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender {
if([#"segue_YOUR_SEGUE_NAME" isEqualToString:[segue identifier]]) {
if([[segue destinationViewController] isKindOfClass:
[SecondViewController class]]) {
SecondViewController *dest = (SecondViewController*)[segue
destinationViewController];
dest.firstVC = self;
}
}
}
Now, in your second view controller, you can do two things, as I already stated:
[self dismissViewControllerAnimated:YES
completion:^{
[self.firstVC someMethod];
}];
OR...
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.firstVC someMethod];
}
To start I am building an app to learn the basics of Objective-C. If there is anything unclear please let me know and I will edit my question.
The app is supposed to have the next functionality.
Open the camera preview when the app is executed. On the top there is a button to go to a TemplateController where the user can select an array of frames to select from a UICollectionView. User selects the Template and returns to the Camera Preview. User takes a picture and the picture with the frame selected is shown in the PreviewController. If the user doesn't like the frame and wants to switch it for another one. PreviewController has button on top to go to the TemplateController, select the frame and go back again to the PreviewController with the new frame.
I do not want to create an object for the frame everytime. I want the AppDelegate to hold that object. To keep it alive per say?(sorry, English is not my mother tongue).
I was thinking to use NSUserDefaults BUT I really want to do it using the AppDelegate. So at this point NSUserDefaults is not an option.
Now, I am using storyboards with a navigation controller. A screenshot is available here
Right now when I pass from the TemplateController to my PreviewController my code looks like this:
Reaching TemplateController from MainController or PreviewController
- (IBAction)showFrameSelector:(id)sender
{
UIStoryboard *storyboard;
storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
TemplateController *templateController = [storyboard instantiateViewControllerWithIdentifier:#"TemplateController"];
templateController.frameDelegate = self;
[self presentViewController:templateController animated:YES completion:nil];
}
Passing the data from TemplateController to its controller's destiny (Either MainController or PreviewController)
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
_selectedLabelStr = [self.frameImages[indexPath.section] objectAtIndex:indexPath.row];
[self.collectionView deselectItemAtIndexPath:indexPath animated:NO];
[self dismissViewControllerAnimated:YES completion:^{
if ([self.frameDelegate respondsToSelector:#selector(templateControllerLoadFrame:)])
{
[self.frameDelegate performSelector:#selector(templateControllerLoadFrame:) withObject:self];
}
}];
}
This loads the selected frame in PreviewController
- (void)templateControllerLoadFrame:(TemplateController *)sender
{
UIImage *tmp = [UIImage imageNamed:sender.selectedLabelStr];
_frameImageView.image = tmp;
}
My problem is, I don't have very clear what changes I have to do on the AppDelegate(it is untouched right now). What would be the best approach to accomplish this?
Main issue is when Tamplate is chosen before taking the still image. If I select the frame after taking the picture then it displays.
I am not certain that I understand your question. Stuffing an object into the app delegate solution may not be the best way forward. In fact I believe you ought to look at the delegation pattern that is used by Apple to communicate between view controllers. Please note that you appear to be doing half of the delegate pattern already. For example you make your PreviewController a frameDelegate of the TemplateController.
So I would think you'd have something like the following to transfer information from TemplateController back to the PreviewController. Note that I've included prepare for segue as that is a common pattern to push a data object forward (it will be called if you connect a segue from the PreviewController to the TemplateController and in your action method call performSegueWithIdentifier:#"SegueTitle"). Use of the "templateControllerDidFinish" delegation method is a common pattern used to push information back from TemplateController when it closes.
TemplateController.h
#class TemplateController;
#protocol TemplateControllerDelegate <NSObject>
-(void) templateControllerDidFinish :(TemplateController*)controller;
#end
#interface TemplateController : UIViewController
#property (nonatomic, weak) id <TemplateControllerDelegate>delegate;
...
#end
TemplateController.m
//! The internals for this method can also be called from wherever in your code you need to dismiss the TemplateController by copying the internal
-(IBAction)doneButtonAction:(id)sender
{
__weak TemplateController*weakSelf = self;
[self dismissViewControllerAnimated:YES completion:^{
[self.delegate templateControllerDidFinish:weakSelf];
}];
}
PreviewController.h
#import "TemplateController.h"
#interface PreviewController<TemplateControllerDelegate>
...
#end
PreviewController.m
#implementation
...
-(void) templateControllerDidFinish :(TemplateController*)controller
{
self.dataProperty = controller.someImportantData;
...
}
...
-(void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
if ( [[segue identifier]isEqualToString:#""] )
{
TemplateController *tc = [segue destinationViewController];
tc.delegate = self;
tc.data = [someDataObjectFromPreviewController];
}
}
To fix this situation a bit more:
Add a segue from the PreviewController to the TemplateController
(Ctrl-drag from Preview view controller to the Template Controller
in the document outline mode)
Name the segue identifier in the identity inspector
Change your code that presents the view controller from:
(IBAction)showFrameSelector:(id)sender
{
UIStoryboard *storyboard;
storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
TemplateController *templateController = [storyboard instantiateViewControllerWithIdentifier:#"TemplateController"];
templateController.frameDelegate = self;
[self presentViewController:templateController animated:YES completion:nil];
}
to
- (IBAction)showFrameSelector:(id)sender
{
[self performSegueWithIdentifier:#"SegueTitle"];
}
Add your data object to the target view controller as noted in prepareForSegue and you will be in good shape. Then use the delegate method to catch any data returned from your template (just add the data as properties to the controller and you should be golden)
You can see a better example of this delegation in a utility project template from Xcode (I just keyed this in..) I hope this information helps. You can get more information at these resources and also by searching Google and SO for iOS delegation :
Concepts in Objective C (Delegates and Data Sources)
Cocoa Core Competencies
Major head-scratcher all day on this one :-(
I have an instance of a UIPageViewController that does not appear to be firing the delegate method:
-(UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
I have tried various methods of displaying the UIPageViewController and have settled on a programatic approach (as opposed to a Storyboard one) that appears to be working correctly, with one exception... when rotating the iPad to landscape the spine does not appear mid-point as expected. I simply cannot find out why the delegate method does not get called.
Code Explanation (simplified for example)
Consider three classes as follows:
RootViewController - loaded when the app starts
PageViewController - loaded by RootViewController upon user initiation
PageContentViewController - loaded by PageViewController when pages are needed
Fairly self-explanatory. The RootViewController is loaded by the app upon launch. When the user taps an image within this view controller's view (think magazine cover opening a magazine) it launches the PageViewController as follows:
PageViewController *pvc = [[PageViewController alloc] initWithNibName:#"PageView"
bundle:[NSBundle mainBundle]];
pvc.view.frame = self.view.bounds;
[self.view addSubview:pvc.view];
In the actual app there is animation etc to make the transition all nice, but essentially the PageViewController's view is loaded and takes fullscreen.
PageViewController
This is the workhorse (only relevant methods shown). I have tried various examples from the infinite world of Google and written directly from the Apple docs...
#interface PageViewController : UIViewController <UIPageViewControllerDelegate, UIPageViewControllerDataSource>
#property (nonatomic, strong) UIPageViewController *pageViewController;
#property (nonatomic, strong) NSMutableArray *modelArray;
#end
#implementation TXCategoryController
-(void)viewDidLoad
{
[super viewDidLoad];
// Simple model for demo
self.modelArray = [NSMutableArray alloc] init];
for (int i=1; i<=20; i++)
[self.modelArray addObject:[NSString stringWithFormat:#"Page: %d", i]];
self.pageViewController = [[UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
self.pageViewController.delegate = self;
self.pageViewController.dataSource = self;
PageContentViewController *startupVC = [[PageContentViewController alloc] initWithNibName:#"PageContent" bundle:nil];
startupVC.pageLabel = [self.modelArray objectAtIndex:0];
[self.pageViewController setViewControllers:[NSArray arrayWithObject:startupVC]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil];
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
[self.pageViewController didMoveToParentViewController:self];
self.pageViewController.view.frame = self.view.bounds;
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
{
// Relevant code to add another view...
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
{
// Relevant code to add another view...
}
-(UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
// Setting a break point in here - never gets called
if (UIInterfaceOrientationIsPortrait(orientation))
{
// Relevant code to create view...
return UIPageViewControllerSpineLocationMin;
}
// Relevant code to create 2 views for side-by-side display and
// set those views using self.pageViewController setViewControllers:
return UIPageViewControllerSpineLocationMid
}
#end
This all works perfectly well as I mentioned earlier. The PageViewController's view gets shown. I can swipe pages left and right in both portrait and landscape and the respective page number appears. However, I don't ever see two pages side-by-side in landscape view. Setting a breakpoint in the spineLocationForInterfaceOrientation delegate method never gets called.
This is such a head-scratcher I have burned out of ideas on how to debug/solve the problem. It almost behaves like the UIPageViewController isn't responding to the orientation changes of the device and therefore isn't firing off the delegate method. However, the view gets resized correctly (but that could be just the UIView autoresizing masks handling that change).
If I create a brand new project with just this code (and appropriate XIb's etc) it works perfectly fine. So something somewhere in my actual project is causing this. I have no idea where to continue looking.
As usual, any and all help would be very much appreciated.
Side Note
I wanted to add the tag 'uipageviewcontrollerspinelocation' but couldn't because it was too long and I didn't have enough reputation (1500 required). I think this is a devious ploy on Apple's part to avoid certain tags in Stackoverflow... ;-)
Finally found the problem. It was something of a red herring in its symptoms, but related just the same.
Putting a break point in the shouldAutorotateToInterfaceOrientation: method was a natural test to see if the UIViewController was even getting a rotation notification. It wasn't which led me to Apple's technical Q&A on the issue: http://developer.apple.com/library/ios/#qa/qa1688/_index.html
The most relevant point in there was:
The view controller's UIView property is embedded inside UIWindow but alongside an additional view controller.
Unfortunately, Apple, in its traditional documentation style, doesn't provide an answer, merely confirmation of the problem. But an answer on Stack Overflow yielded the next clue:
Animate change of view controllers without using navigation controller stack, subviews or modal controllers?
Although my RootViewController was loading the PageViewController, I was doing it as a subview to the main view. This meant I had two UIViewController's in which only the parent would respond to changes.
The solution to get the PageViewController to listen to the orientation changes (thus triggering the associated spine delegate method) was to remove addSubview: and instead present the view controller from RootViewController:
[self presentViewController:pac animated:YES completion:NULL];
Once that was done, the orientation changes were being picked up and the PageViewController was firing the delegate method for spine position. Only one minor detail to consider. If the view was launched in landscape, the view was still displaying portrait until rotated to portrait and back to landscape.
That was easily tweaked by editing viewDidLoad as follows:
PageContentViewController *page1 = [[PageContentViewController alloc] initWithNibName:#"PageContent" bundle:nil];
NSDictionary *pageViewOptions = nil;
NSMutableArray *pagesArray = [NSMutableArray array];
if (IS_IPAD && UIInterfaceOrientationIsLandscape(self.interfaceOrientation))
{
pageViewOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:UIPageViewControllerSpineLocationMid]
forKey:UIPageViewControllerOptionSpineLocationKey];
PageContentViewController *page2 = [[PageContentViewController alloc] initWithNibName:#"PageContent" bundle:nil];
[pagesArray addObject:page1];
[pagesArray addObject:page2];
}
else
{
[pagesArray addObject:page1];
}
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:pageViewOptions];
self.pageViewController.delegate = self;
[self.pageViewController setViewControllers:pagesArray
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
Job done and problem solved.
I've got two classes. ManagingViewController, a subclass of NSViewController, and ViewController, a subclass auf ManagingViewController. In Viewcontroller I've got a NSTextField which I want to become the firstResponder, but I didn't manage that.
So it is nearly the same like the Chapter 29 in Hillegass' book Cocoa Programming for Mac OS X (Download of the book's examples) except of an NSTextField which is set to firstResponder.
Can anybody point me to the correct way?
You need to set the text field as the first responder by using -[NSWindow makeFirstResponder:].
Since this is an NSWindow method, it only makes sense after you’ve added the corresponding view to the window, i.e., after you’ve added the view as a subview inside the window view hierarchy. In the book’s example, this happens when you set the view as the content view of the box inside the window. For example:
- (void)displayViewController:(ManagingViewController *vc) {
// Try to end editing
NSWindow *w = [box window];
…
// Put the view in the box
NSView *v = [vc view];
[box setContentView:v];
// Set the first responder
if ([vc class] == [ViewController class]) {
[w makeFirstResponder:[(ViewController *)vc myTextField]];
}
}
This assumes ViewController exposes a getter method called -myTextField.
You can make this more generic by having your view controllers expose a method that returns the object that the view controller recommends as the first responder. Something like:
#interface ManagingViewController : NSViewController
…
- (NSResponder *)recommendedFirstResponder;
#end
#implementation ManagingViewController
…
- (NSResponder *)recommendedFirstResponder { return nil; }
#end
And, in your concrete subclasses of ManagingViewController, have -recommendedFirstResponder return the object that should be the window’s first responder:
#implementation ViewController
…
- (NSResponder *)recommendedFirstResponder { return myTextField; }
#end
Having done that, you can change your -displayViewController: to something like:
- (void)displayViewController:(ManagingViewController *vc) {
// Try to end editing
NSWindow *w = [box window];
…
// Put the view in the box
NSView *v = [vc view];
[box setContentView:v];
// Set the first responder
NSResponder *recommendedResponder = [vc recommendedFirstResponder];
if (recommendedResponder) [w makeFirstResponder:recommendedResponder];
}
Have you tried [[myTextField window] makeFirstResponder:myTextField]; ?
simple. Goto you xib file in interface builder. right click the first responder field. it will show the connection , remove the connection and connect it to the desired responder. let me know if this works
Currently my application has a single navigation screen that allows the user to select other views. When clicked the navController simply does a push of the specific view controller in question. This works great if I never come back to the same view again expecting it to be reloaded.
In the code below I push a view controller when requested.
- (void)optionClicked:(NSString *)optionName
{
if ([#"First" isEqualToString:optionName]) {
[navController pushViewController:firstController animated:YES];
} else if ([#"Next" isEqualToString:optionName]) {
[navController pushViewController:nextController animated:YES];
}
}
When done with a view I simply pop it from the stack. But the next time a user selects this same option from the menu it's not loaded "clean" and this is where my question comes in. How can I load a view controller fresh each time it's pushed on the stack?
You have to reinitialize the viewcontroller.
if ([#"First" isEqualToString:optionName]) {
if (firstController)
{ [firstController release]; } // assuming you've got a retain on it.
firstController = [[MyViewControllerSubclass alloc] init];
[navController pushViewController:firstController animated:YES];
}
In this situation I would suggest using a property with retain on it. #property (nonatomic, retain) MyViewControllerSubclass *firstController;
that way you can use self.firstController = [[[MyViewControllerSubclass alloc] init] autorelease]; and the memory management is mostly done for you. (Although you still have to release in dealloc.)