I have the following files:
Main.storyboard
This is the same as the default storyboard created when creating a new project with a few additions: There is a button in the view which is connected to the button outlet of the view controller (instance of ViewController) and the doTheThing: action on the view controller.
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#property (weak) IBOutlet NSButton *button;
#property (weak) NSViewController *controller;
#end
#implementation ViewController
- (IBAction)doTheThing:(id)sender {
if (self.controller) {
NSLog(#"Removing %#", self.controller);
[self.controller.view removeFromSuperview];
[self.controller removeFromParentViewController];
} else {
self.controller = [[NSStoryboard storyboardWithName:#"Another" bundle:[NSBundle mainBundle]] instantiateInitialController];
[self addChildViewController:self.controller];
[self.view addSubview:self.controller.view];
NSLog(#"Adding %#", self.controller);
}
}
#end
Another.storyboard
A simple storyboard containing a single scene (view controller + view) that is set to the initial controller and is an instance of AnotherViewController. There is a label in the view that is not connected to any outlet.
AnotherViewController.m
#import "AnotherViewController.h"
#interface AnotherViewController ()
#property (weak) IBOutlet NSTextField *label;
#end
#implementation AnotherViewController
- (void)dealloc {
NSLog(#"Deallocing AnotherViewController %#", self);
}
#end
When I run the app and click the button, it adds the view controller and view from Another.storyboard, and when I click the button again they are removed and the instance of AnotherViewController is deallocated.
However, if I connect the label in Another.storyboard to the label outlet on the AnotherViewController, the deallocation never occurs. Why is this and what can I do to fix it?
Edit: I do have a few workarounds, but they aren't very desirable and I would prefer to understand why the recommended way (storyboards and outlets) isn't working properly.
Undesirable workaround 1: Do the same thing but load the view controller and view from a XIB file. This works as expected, but ideally I would be able to do the same thing using storyboards.
Undesirable workaround 2: Bind all my outlets manually in code in the view controller's viewDidLoad method. This is just tedious and ugly as it requires iterating through all the view's subviews and comparing identifiers.
Apple have confirmed (via bug report) that this is a known issue and will be fixed in OS X 10.10.3.
Related
I'm working with the C4 framework on a project and I'm having a bit of trouble with using the gesture controls and calling methods from C4WorkSpace.
I found this link that had to do with this problem but it didn't seem to stop my program from throwing an error.
A little background: I'm creating a custom button class that has 4 properties: UIColor, C4Shape and 2 floats (x/y position). I'm storing the custom button objects in an array and have this bit of code going to add a gesture to each button and add it to the canvas.
C4WorkSpace.m
for (button in buttonArray){
[button.shape addGesture:TAP name:#"tapGesture" action:#"tapped"];
[self.canvas addShape:button.shape];
[self listenFor:#"tapped" fromObject:button andRunMethod:#"doThis:"];
}
And I also have a function outside my '-(void)setup' that just prints a log message:
-(void)doThis:(NSNotification *)notification{
NSLog(#"notification test");
}
I have extended my C4Shape class with a category and when I call this method it works fine:
#import "C4Shape+myC4Shape.h"
#implementation C4Shape (myC4Shape)
-(void)printTest{
NSLog(#"this is a print test");
}
#end
The error I'm getting gets thrown when I try and tap on a button, it says: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[C4Shape tapped]: unrecognized selector sent to instance 0x931e9c0'
I have my "action" set to 'tapped', but that's just something I made up. I know this usually calls a method inside the shape class, but I'm trying to call a method that isn't in the C4Shape class but on the main WorkSpace. I'm not sure what's causing this or what I'm doing wrong? Is there something specific I need to be writing in the 'action' part of the method?
this Obj-C stuff can be confusing at first. Hopefully this will help.
I notice two things for you to change.
The first is in regards to your TAP gesture. When you add this to your code you're actually telling the gesture to recognize a TAP and then run the tapped method. However, this method doesn't exist (hence the -[C4Shape tapped]: unrecognized selector sent to instance 0x931e9c0). You should have a method in your code called -(void)tapped{}; for this message to go away.
The following code illustrates this:
#implementation C4WorkSpace
-(void)setup {
[self addGesture:TAP name:#"tap" action:#"tapped"];
}
-(void)tapped {
C4Log(#"tapped!");
}
#end
Putting that into a shape is quite similar.
Second, for the subclassing, C4Shape (and all other visual objects) inherit from C4Control so I would suggest that subclassing C4Control isn't the best approach. For creating your own custom button with a C4Shape I would subclass C4Shape directly.
The above code would look exactly the same for a subclassed C4Shape except that if you created a MyShape subclass would have #implementation MyShape instead.
Finally, you want to add a little broadcast message to your tap method so that the canvas can listen for it... All in all your .h and .m files for your subclass should look like this:
.h
#import "C4Shape.h"
#interface MyShape : C4Shape
#end
.m
#import "MyShape.h"
#implementation MyShape
-(void)setup {
[self addGesture:TAP name:#"tap" action:#"tapped"];
}
-(void)tapped {
[self postNotification:#"tapNotification"];
}
#end
Finally, with those two files in your project you can add the following to your workspace:
#import "C4WorkSpace.h"
#import "MyShape.h"
#implementation C4WorkSpace
-(void)setup {
MyShape *m = [[MyShape alloc] init];
[m rect:CGRectMake(0, 0, 100, 100)];
m.center = self.canvas.center;
[self.canvas addShape:m];
[self listenFor:#"tapNotification" fromObject:m andRunMethod:#"heardTap:"];
}
-(void)heardTap:(NSNotification *)aNotification {
MyShape *notificationShape = (MyShape *)[aNotification object];
C4Log(#"%4.2f,%4.2f",notificationShape.center.x,notificationShape.center.y);
C4Log(#"%#",notificationShape.strokeColor);
}
#end
Here's a link to a gist with the 3 files you'll need for this to run.
Button Subclass with Canvas Listener
PS thanks for giving C4 a try!
Oh, and this is the output I get in Xcode's console when I tap on the square:
[C4Log] 384.00,512.00
[C4Log] UIDeviceRGBColorSpace 1 0.1 0.1 1
This gives me the x and y coordinates, as well as the strokeColor of the "button" without having to add these as objects to the MyShape class (because they already exist in C4Shape).
I have a parent view and a modal view with a text box. What I am trying to do is pass whatever is entered into the text box from the modal view and then pass it to a label in the parent view which updates the label to what was entered. I hope that made any sense.
I have been pulling my hair out for a couple of weeks trying to figure this out with no luck. I found many examples and tutorials about segues and passing between views that are being pushed but nothing about modal views and passing back to the parent view.
I have been trying to understand this and need a good example. I kind of understand the prepare for segue concept but for some reason, I just can't figure this one out. Any help on this would be much appreciated and you would be my hero for life lol.
In my project that uses segues, here's how I did it (note that I'm new to iOS, so there's probably "better" ways, and this may be obvious to the iOS veterans):
The short version: define a callback protocol in your modal view controller's .h file. When your modal view controller closes, it checks to see if the presenter implements that protocol and invokes those methods to pass along the data.
So like you said, let's say your modal view controller just gathers a single string value from the user and then they click OK or Cancel. That class might look like this:
#interface MyModalViewController : UIViewController
...
#end
I'm suggesting you add a protocol like this to the same header:
#protocol MyModalViewControllerCallback
-(void) userCancelledMyModalViewController:(MyModalViewController*)vc;
-(void) userAcceptedMyModalViewController:(MyModalViewController*)vc
withInput:(NSString*)s;
#end
Then in MyModalViewController.m, you add a viewDidDisappear with code similar to this:
-(void) viewDidDisappear:(BOOL)animated {
UIViewController* presenter = self.presentingViewController;
// If the presenter is a UINavigationController then we assume that we're
// notifying whichever UIViewController is on the top of the stack.
if ([presenter isKindOfClass:[UINavigationController class]]) {
presenter = [(UINavigationController*)presenter topViewController];
}
if ([presenter conformsToProtocol:#protocol(MyModalViewControllerCallback)]) {
// Assumes the presence of an "accepted" ivar that knows whether they
// accepted or cancelled, and a "data" ivar that has the data that the
// user entered.
if (accepted) {
[presenter userAcceptedMyModalViewController:self withInput:data];
}
else {
[presenter userCancelledMyModalViewController:self];
}
}
[super viewDidDisappear:animated];
}
And finally in the parent view, you implement the new #protocol, e.g. in the .h:
#interface MyParentViewController : UIViewController <MyModalViewControllerCallback>
...
#end
and in the .m:
#implementation MyParentViewController
...
-(void) userCancelledMyModalViewController:(MyModalViewController*)vc {
// Update the text field with something like "They clicked cancel!"
}
-(void) userAcceptedMyModalViewController:(MyModalViewController*)vc
withInput:(NSString*)s {
// Update the text field with the value in s
}
...
#end
I am trying to build a view where a user can select an image and it is retained within that view after navigating away from the view and coming back again. As a simple test app, I have created two view controllers. The first view controller has a forward button and uses a modal segue to go to the second view controller. The second view controller has a back button (modal segue back to first view controller) a Choose button and a Image button. So far I have configured the choose button to use the imagepickercontroller to select an image from the library and display onto the button which works no problem. The issue is that when i press the back button and the forward again back to the same screen the image isn't retained i have to reselect it. The intention is for the user to be able to take a picture or video of themselves performing an action and then select it within this app and have the picture displayed there for future reference.
My coding is as follows:
ViewController.h
#interface ViewController : UIViewController
<UIImagePickerControllerDelegate, UINavigationControllerDelegate>
{
IBOutlet UIButton *selectFromPhotoLibraryButton;
IBOutlet UIButton *displayPictureButton;
}
#property (nonatomic, retain) UIButton *selectFromPhotoLibraryButton;
#property (nonatomic, retain) UIButton *displayPictureButton;
- (IBAction)selectPicture;
#end
ViewController.m
#implementation ViewController
#synthesize selectFromPhotoLibraryButton;
#synthesize displayPictureButton;
-(IBAction)selectPicture
{
if([UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypePhotoLibrary])
{
UIImagePickerController *picker = [[UIImagePickerController alloc]init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentModalViewController:picker animated:YES];
[picker release];
}
}
- (void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo: (NSDictionary *)info
{
UIImage *image = [[info objectForKey:UIImagePickerControllerOriginalImage] retain];
[displayPictureButton setImage:image forState:UIControlStateNormal];
[self dismissModalViewControllerAnimated:YES];
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *) picker
{
[picker dismissModalViewControllerAnimated:YES];
}
Please let me know if you require any of the other bits of code (its only the default app stuff, no further modification).
As looking to your problem
I think you have to store selected image in NSUserDefault as archived data or something like that.
When u come back to second view then check first in NSUserDefault for image is saved or not.
If u got it then assign it to UIImage object by unarchiving.
As I have not implemented it so can't be sure.
I've just set something like this up myself, I have the chosen image immediately placed in a UIImageView.
First, set up your view controller as a UIImagePickerDelegate:
<UIImagePickerControllerDelegate, UINavigationControllerDelegate>
Then wherever you want to init the UIImagePicker, alloc, init and set its delegate to self:
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
[imagePicker setDelegate:self];
[self presentViewController:imagePicker animated:YES completion:nil];
And then I have the image picker storing the UIImage into the ImageView and also into the NSUserDefaults.
- (void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo: (NSDictionary *)info {
UIImage *image = [[info objectForKey:UIImagePickerControllerOriginalImage] retain];
[userDefaults setObject:image forKey:#"image"];
[userDefaults synchronize];
[self dismissViewControllerAnimated:YES completion:^{
[self updatePreviewImage:image];
}];
}
- (void) imagePickerControllerDidCancel:(UIImagePickerController *) picker {
[self dismissViewControllerAnimated:YES completion:nil];
}
I hope this works for you, and anyone else that view this post.
In Xcode 4 I've created a SplitView project, so I have a DetailViewController and a RootViewController.
In RootViewController's viewDidLoad(), I have added a cancel button on the left side like this
// add a cancel button on the left side
cancelButton = [[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self action:#selector(cancelAction:)] autorelease];
self.navigationItem.leftBarButtonItem = cancelButton;
When the RootViewController is displayed and I click on the cancel button, I want to close the RootViewController. In my cancelButton handler I've tried calling
#if 0
[self dismissModalViewControllerAnimated:YES];
#else
[self.navigationController popViewControllerAnimated:YES];
#endif
but neither one of them work for me. I've tried googling for hours but have still not found a solution.
Can someone please tell me how to close the RootViewController?
thanks for your help
I'm assuming you're trying to close popover of RootViewController in portrait orientation because in landscape it doesn't make sense.
You can do it like this:
1) cancel button handler:
-(void)cancelAction {
[self.detailViewController.popoverController dismissPopoverAnimated:YES];
}
2) popoverController is private in detailViewController so you must make it public (or add method to detailview that will have access to popoverController property and call this method from cancelAction in RootViewController):
2.1) delete
#property (nonatomic, retain) UIPopoverController *popoverController;
from
#interface DetailViewController ()
...
#end
2.2) add
#property (nonatomic, retain) UIPopoverController *popoverController;
inside
#interface DetailViewController : UIViewController
How do I pop up an UIAlertView when the back button of a UINavigationBar (controlled by a UINavigationController) was tapped? Under certain conditions, I want to ask the user an "Are you sure?" type of question so he could either abort the action and stay on the current view or pop the navigation stack and go to the parent view.
The most appealing approach I found was to override ShouldPopItem() on UINavigationBar's Delegate.
Now, there is a quite similar question here: iphone navigationController : wait for uialertview response before to quit the current view
There are also a few other questions of similar nature, for example here:
Checking if a UIViewController is about to get Popped from a navigation stack?
and How to tell when back button is pressed in a UINavigationControllerStack
All of these state "subclass UINavigationController" as possible answers.
Then there is this one that reads like subclassing UINavigationController is generally not a good idea:
Monotouch: UINavigationController, override initWithRootViewController
The apple docs also say that UINavigationController is not intended to be subclassed.
A few others state that overriding ShouldPopItem() is not even possible when using a UINavigationController as that does not allow to assign a custom/subclassed UINavigationBarDelegate to the UINavigationBar.
None of my attempts of subclassing worked, my custom Delegate was not accepted.
I also read somewhere that it might be possible to implement ShouldPopItem() within my custom UINavigationController since it assigns itself as Delegate of its UINavigationBar.
Not much of a surprise, this didn't work. How would a subclass of UINavigationController know of the Methods belonging to UINavigationBarDelegate. It was rejected: "no suitable method found to override". Removing the "override" keyword compiled, but the method is ignored completely (as expected). I think, with Obj-C one could implement several Protocols (similar to Interfaces in C# AFAIK) to achieve that. Unfortunately, UINavigationBarDelegate is not an Interface but a Class in MonoTouch, so that seems impossible.
I'm pretty much lost here. How to override ShouldPopItem() on UINavigationBar's Delegate when it is controlled by a UINavigationController? Or is there any other way to pop up an UIAlertView and wait for it's result before possibly popping the navigation stack?
This post is a bit old, but in case you're still interested in a solution (still involves subclassing though):
This implements a "Are you sure you want to Quit?" alert when the back button is pressed, modified from the code here: http://www.hanspinckaers.com/custom-action-on-back-button-uinavigationcontroller/
Turns out if you implement the UINavigationBarDelegate in the CustomNavigationController, you can make use of the shouldPopItem method:
CustomNavigationController.h :
#import <Foundation/Foundation.h>
#interface CustomNavigationController : UINavigationController <UIAlertViewDelegate, UINavigationBarDelegate> {
BOOL alertViewClicked;
BOOL regularPop;
}
#end
CustomNavigationController.m :
#import "CustomNavigationController.h"
#import "SettingsTableController.h"
#implementation CustomNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if (regularPop) {
regularPop = FALSE;
return YES;
}
if (alertViewClicked) {
alertViewClicked = FALSE;
return YES;
}
if ([self.topViewController isMemberOfClass:[SettingsTableViewController class]]) {
UIAlertView * exitAlert = [[[UIAlertView alloc] initWithTitle:#"Are you sure you want to quit?" message:nil delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Yes", nil] autorelease];
[exitAlert show];
return NO;
}
else {
regularPop = TRUE;
[self popViewControllerAnimated:YES];
return NO;
}
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
//Cancel button
}
else if (buttonIndex == 1) {
//Yes button
alertViewClicked = TRUE;
[self popViewControllerAnimated:YES];
}
}
#end
The weird logic with the "regularPop" bool is because for some reason just returning "YES" on shouldPopItem only pops the navbar, not the view associated with the navBar - for that to happen you have to directly call popViewControllerAnimated (which then calls shouldPopItem as part of its logic.)
For reference, the route I took after giving up on ShouldPopItem() is to replace the back button with a UIBarButtonItem that has a custom UIButton assigned as it's CustomView. The UIButton is crafted to look like the original back button using two images for the normal and the pressed state. Finally, hiding the original back button is required.
Way too much code for what it's supposed to do. So yeah, thanks Apple.
BTW: Another possibility is creating a UIButton with the secret UIButtonType 101 (which is actually the back button) but I avoided this as it may break at any later iOS version.
Override only UINavigationBarDelegate methods in a UINavigationController subclass and it should simply work. Be cautious that the protocol methods are also called when you push or pop a view controller from inside your code and not only when the back button is pressed. This is because them are push/pop notifications not button pressed actions.
Xamarin does provide the IUINavigationBarDelegate interface to allow you to implement the UINavigationBarDelegate as part of your custom UINavigationController class.
The interface however does not require that the ShouldPopItem method be implemented. All the interface does is add the appropriate Protocol attribute to the class so it can be used as a UINavigationBarDelegate.
So in addition you need to add the ShouldPopItem declaration to the class as follows:
[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
I've merged this solution with a native Obj-C solution. This is the way I'm currently handling the cancellation of the BACK button in iOS
It seems that it is possible to handle the shouldPopItem method of the NavigationBar in this way:
Subclass a UINavigationController
Mark your custom UINavigationController with the IUINavigationBarDelegate
Add this method with the Export attribute
[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
Now you can handle popping in the ShoulPopItem method. An example to this is to create an interface like this
public interface INavigationBackButton
{
// This method should return TRUE to cancel the "back operation" or "FALSE" to allow normal back
bool BackButtonPressed();
}
Then mark your UIViewController which needs to handle the back button with this interface. Implement something like this
public bool BackButtonPressed()
{
bool needToCancel = // Put your logic here. Remember to return true to CANCEL the back operation (like in Android)
return needToCancel;
}
Then in your ShouldPopItem Implementation have something like this
tanks to: https://github.com/onegray/UIViewController-BackButtonHandler/blob/master/UIViewController%2BBackButtonHandler.m
[Export("navigationBar:shouldPopItem:")]
public bool ShouldPopItem(UINavigationBar navigationBar, UINavigationItem item)
{
if (this.ViewControllers.Length < this.NavigationBar.Items.Length)
return true;
bool shouldPop = true;
UIViewController controller = this.TopViewController;
if (controller is INavigationBackButton)
shouldPop = !((INavigationBackButton)controller).BackButtonPressed();
if (shouldPop)
{
//MonoTouch.CoreFoundation.DispatchQueue.DispatchAsync
CoreFoundation.DispatchQueue.MainQueue.DispatchAsync(
() =>
{
PopViewController(true);
});
}
else
{
// Workaround for iOS7.1. Thanks to #boliva - http://stackoverflow.com/posts/comments/34452906
foreach (UIView subview in this.NavigationBar.Subviews)
{
if(subview.Alpha < 1f)
UIView.Animate(.25f, () => subview.Alpha = 1);
}
}
return false;
}