UINavigationBar topItem/items seems to double-pop on back - objective-c

I am managing my own UINavigationBar. I need to do this due to extensive skinning. The documentation for UINavigationController warns that there are limitations to skinning the UINavigationBar when used with a UINavigationController.
I have put in extensive logging and from everything I can tell, pushing the "Back" button in the UINavigationController pops two items off of of the stack instead of one. I get a single delegate callback telling me that it is removing the logical item, but it actually removes that one and one more.
The item added to the UINavigationController in awakeFromNib should never be removed. It is being removed for some reason.
There are two similar questions, but neither have satisfactory answers. The two questions are:
UINavigationBar .items accessor doesn't return the current UINavigationItem
UINavigationBar seems to pop 2 items off stack on "back"
- (void)awakeFromNib {
[headerView setDelegate: self];
[headerView pushNavigationItem: tableDisplay animated: NO];
}
- (void) selectedStory: (NSNotification *)not {
[headerView pushNavigationItem: base animated: NO];
NSLog(#"Selected story: %#", base);
}
- (void) baseNav {
NSLog(#"Current items: %#", [headerView items]);
BaseInnerItem *current = (BaseInnerItem *)[headerView topItem];
[self addSubview: [current view]];
}
- (BOOL)navigationBar: (UINavigationBar *)navigationBar shouldPushItem: (UINavigationItem *)item {
return YES;
}
- (BOOL)navigationBar: (UINavigationBar *)navigationBar shouldPopItem: (UINavigationItem *)item {
return YES;
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item {
NSLog(#"didPushItem: %#", item);
[self baseNav];
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item {
NSLog(#"didPopItem: %#", item);
[self baseNav];
}
Edited to add relevant debugging from a single run:
2010-10-13 02:12:45.911 Remix2[17037:207] didPushItem: <TableDisplay: 0x5d41cc0>
2010-10-13 02:12:45.912 Remix2[17037:207] Current items: (
"<TableDisplay: 0x5d41cc0>"
)
2010-10-13 02:12:49.020 Remix2[17037:207] didPushItem: <WebDisplay: 0x591a590>
2010-10-13 02:12:49.021 Remix2[17037:207] Current items: (
"<TableDisplay: 0x5d41cc0>",
"<WebDisplay: 0x591a590>"
)
2010-10-13 02:12:49.023 Remix2[17037:207] Selected story: <WebDisplay: 0x591a590>
2010-10-13 02:12:59.498 Remix2[17037:207] didPopItem: <WebDisplay: 0x591a590>
2010-10-13 02:12:59.499 Remix2[17037:207] Current items: (
)

You always have to call [super awakeFromNib] when your subclass implements that method, per the documentation for -awakeFromNib:
You must call the super implementation of awakeFromNib to give parent classes the opportunity to perform any additional initialization they require
Importantly, however, ...
I don't understand why you have to actually manage your own navigation bar. If you subclass UINavigationBar and only override certain drawing or layout methods such as -drawRect:, -layoutSubviews, etc., then all of the logic behind managing the navigation bar in a navigation controller will just fall back on the original UINaviationBar class.
I've had to do extensive view customization for almost every major UIKit class, but I always left the complicated logic to the original classes, overriding only drawing methods to customize the look and feel.
Incidentally, it's actually much easier to skin an entire app without subclassing at all if all you're doing is using custom image assets. By setting a layer's contents property, you can either customize the look and feel of a UIView-based class on an as-needed basis or throughout your entire app:
#import <QuartzCore/QuartzCore.h>
...
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage * navigationBarContents = [UIImage imageNamed:#"navigation-bar"];
self.navigationController.navigationBar.layer.contents =
(id)navigationBarContents.CGImage;
}
You can set the contents for any class that inherits from UIView: navigation bars, toolbars, buttons, etc. It's a lot easier to manage this way without having to subclass at all.

This appears to be a bug in the implementation of -[UINavigationBar items]
When called from inside the -navigationBar:didPopItem: delegate method, it will omit the last object. You can check this by calling [navigationBar valueForKey:#"_itemStack"] to retrieve the underlying array and see that the expected items are still there.
Adding a dispatch_async inside -navigationBar:didPopItem:method successfully works around the issue in my app.

Related

ViewControllers with TextViews in UIPageViewController

I was trying to learn UIPageViewControllers and hit an Issue which I couldn't resolve.
This is what I tried to do:
Steps:
I simply created 2 view controllers and a page view controller in
StoryBoard.
Then I added some code to the File's Owner of PageViewController to
behave as a dataSource and delegate to itself.
When I ran, things worked well.
I added some buttons, and text fields to the second view controller.
I ran, worked well.
Now I added a text view to the second view controller and ran. When I tried to write something inside the text view, the page control jittered and moved to first view controller.
Has anyone experience this ever?
#interface AMPageViewController : UIPageViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
#end
The implementation:
#import "AMPageViewController.h"
#interface AMPageViewController ()
{
UIViewController *mainController;
UIViewController* socController;
}
#end
#implementation AMPageViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:#"MainStoryboard"
bundle: nil];
mainController = (UIViewController*)[mainStoryboard instantiateViewControllerWithIdentifier: #"First"];
socController = (UIViewController*)[mainStoryboard instantiateViewControllerWithIdentifier: #"Second"];
[self setViewControllers:#[mainController]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil];
self.dataSource = self;
self.delegate = self;
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
if (viewController == socController )
return mainController;
else return nil;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
if (viewController == mainController )
return socController;
else return nil;
}
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
return 2;
}
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
return 0;
}
#end
If you want to download and try the project
I've investigated a lot on this problem.
It seems a bug related to the internal (private) UIScrollView of the UIPageViewController.
If you search on StackOverflow you will find a lot of post with this problem and no solutions...
I seems that the UITextView (which is an UIScrollView and, AFAIR, has an internal UIWebView), sends some strange message to it's superviews chain, that makes the private UIScrollView of the UIPageViewController scrolling to the top-left corner.
I would have tried to block this message using method swizzling, but this is probably not ok for AppStore. So I tried other things.
The final solution is very simple: simply, embed your UITextView inside an UIScrollView!
This is a link to your project updated
If you do so, you'll solve the problem!
Try and let me know
EDIT:
How did I arrive to this solution:
An intuition.
A lot of debug and stack traces had make me think that the problem was related to a bug in the "nesting UIScrollView" system and some messages sent from the inner view to its superview.
UITextView inherits from UIScrollView and has inside an UIWebDocumentView (private) which is another UIScrollView. During the debug I saw a lot of messages (private methods) like "relayout superview" sent to the upper UIScrollView's. So, for some reason, the inner scroll view (UIWebDocumentView?) was sending a message/event to it's superview. This message/event (probably because of a bug) was not stopping to the external UITextView, and was forwarded to the UIScrollView handled by UIPageViewController.
Embedding the UITextView inside a simple UIView was not enough, because UIView forward the message to it's superview if it can't handle.
I thought: UIScrollView probably doesn't (otherwise it wouldn't simple to nest UIScrollViews), so I tried and it worked.
This is all a supposition because I stopped inspecting, I will have a more in-depth look this week.
Build target iOS-7.0.
The scrollview trick wasn't working for me. Tried to embed the textview in a scrollview through storyboard and code but no luck.
Simply delaying the call to the textview did it. Not very elegant, but its the only thing I've gotten to work so far.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.textView becomeFirstResponder];
});
}
Tested, working on my iPhone 5 and my ultra-slow iPhone4. Although its totally possible that whatever implementation detail enables the textview to become the responder could take longer than the set time. So keep in mind this isn't exactly bulletproof.
--EDIT--
Well... it's working on my iPhone 4 beater with a delay of 0.0000000000000001
you did not set before and after view controllers and also look in to first responder for socController

UIGestureRecognizer causing "EXC_BAD_ACCESS" error

Using the GestureRecognizer attached to a view triggers my app to crash with EXC_BAD_ACCESS error. Here's the classes involved
• BoardViewController - Displaying a board (as background) set as rootViewController in the AppDelegate. It instantiates multiple objects of the "TaskViewcontroller".
//BoardViewController.h
#interface BoardViewController : UIViewController {
NSMutableArray* allTaskViews; //for storing taskViews to avoid having them autoreleased
}
//BoardViewController.m - Rootviewcontroller, instantiating TaskViews
- (void)viewDidLoad
{
[super viewDidLoad];
TaskViewController* taskA = [[TaskViewController alloc]init];
[allTaskViews addObject:taskA];
[[self view]addSubview:[taskA view]];
}
• TaskViewController - An indivual box displayed on the board. It should be draggable. Therefore I attached UIPanGestureRecoginzer to its view
- (void)viewDidLoad
{
[super viewDidLoad];
UIPanGestureRecognizer* panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePan:)];
[[self view] addGestureRecognizer:panRecognizer];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
NSLog(#"PAN!");
}
The .xib file is a simple view.
All programming with the gesture recognizer I'd prefer to do in code. Any idea how to fix the error causing the app crash?
The method handlePan is on your view controller, not on your view. You should set the target to self:
UIPanGestureRecognizer* panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePan:)];
EDIT (in response to the edit of the question) As omz has correctly noted, your TaskViewController gets released upon BoardViewController's viewDidLoad: exit. There are two ways of dealing with it:
Fold the handlePan method into the parent view controller, along with the code of viewDidLoad:, or
Make an instance variable for TaskViewController *taskA, rather than making it a local variable.
This is my way to use Gesture Recognizer. I think this way is easy and with low risk.
At first, you drag and drop Gesture Recognizer into the view.
Then, you wire the Gesture Recognizer icon to code.
Finally, you write code for this IBAction like below:
- (IBAction)handlePan:(id)sender {
NSLog(#"PAN!");
}
You can download this project from GitHub and just run it.
https://github.com/weed/p120812_PanGesture

UIPageViewControllerSpineLocation Delegate Method Not Firing

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.

Access int variable from other class

I have two windows, my main window "window" and "help window" all inside my App Delegate. In my main window its view is subclassed and I want to draw a rect inside it. My help window has a rect also but it has an NSTracker on it. What I want to do is draw my rect in my window subclass with the x and y coordinates equal to my NSTracker position. The problem I am having is it crashes when I try to bring in the coordinates, any ideas of what I could be doing wrong? thanks
//My subclass of window is called CutoutView. This is all in draw rect
AppDelegate *controller = [[[NSApp delegate] window] contentView];
xValue = controller.mouseLoc.x;
yValue = controller.mouseLoc.y;
NSRectFillUsingOperation(NSMakeRect(xValue,yValue, 600, 400), NSCompositeClear);
[self update];
- (void)update
{
NSLog(#"test");
[self setNeedsDisplay:YES];
}
//My AppDelegate with the tracker helpView is a reference to the view of my second window "Help Window"
-(void)updateTrackingAreas
{
if(trackingArea != nil) {
[self.helpView removeTrackingArea:trackingArea];
[trackingArea release];
}
opts = (NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self.helpView bounds]
options:opts
owner:self
userInfo:nil];
[self.helpView addTrackingArea:trackingArea];
}
-(void)mouseMoved:(NSEvent *)theEvent
{
mouseLoc = [NSEvent mouseLocation];
NSLog(#"mouseMoved: %f %f", mouseLoc.x, mouseLoc.y);
}
in my CutoutView am i getting the AppController wrong because it is in a different window "helpWindow"? or does it have to do with my int values?
There are many things wrong with your code and it's obvious that you are fundamentally misunderstanding some basic concepts.
Firstly, you state that this code is in your drawRect: method;
AppDelegate *controller = [[[NSApp delegate] window] contentView];
xValue = controller.mouseLoc.x;
yValue = controller.mouseLoc.y;
NSRectFillUsingOperation(NSMakeRect(xValue,yValue, 600, 400), NSCompositeClear);
[self update];
There are several immediate flaws apparent. Firstly, why are you declaring controller to be of type AppController* when the method you are calling (-contentView) returns an NSView?
Your AppController is not a view (at least it should not be!), so you should be declaring the object as such:
NSView* mainView = [[[NSApp delegate] window] contentView];
If you are indeed using a view as a controller then this is completely wrong. See below for my note about MVC.
You don't specify where the mouseLoc property is coming from. We need to see where this is declared, because that will affect whether or not there are problems with it.
Your drawing code calls [self update], which simply tells the view to redraw itself. This will result in an infinite loop because every time the view draws it is forced to redraw. You should never call setNeedsDisplay: from inside drawRect:.
Even after making these changes, this code is very badly structured and the design is broken.
As it stands, your code violates the Model-View-Controller pattern. A views should not have knowledge of other views. You need to restructure things so that your views display properties of your controller without needing knowledge of other views. That means that you must store the mouse location in your controller (or a model object) and use some method for the view to access that information, preferably a datasource protocol or similar. In my answer to this other question I give an example of how to do that.
You need to read the Cocoa Drawing Guide. You also need to learn more core Cocoa concepts as it is clear you are misunderstanding how Cocoa is supposed to work.

Where to call [self addSubView]?

I have a custom view to which I need to add a couple of subviews programmatically. Which is the best place to create and add these subviews? Apple docs say:
If your view class manages one or more integral subviews, do the following: Create those subviews during your view’s initialization sequence.
This is a bit unclear to me. Should I handle that in initWithFrame, initWithCoder or somewhere else?
Note that I'm not talking about controllers here, this is a View that needs to initialize itself.
initWithFrame is the method you call to create a UIView programmatically, while initWithCoder is called when a UIView is instanciated from a XIB.
So it all depends on how you're going to create your containing view.
A way to cover all cases :
- (id) initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])){
[self setUpSubViews];
}
return self;
}
- (id) initWithCoder:(NSCoder*)aDecoder
{
if ((self = [super initWithCoder:aDecoder])){
[self setUpSubViews];//same setupv for both Methods
}
return self;
}
- (void) setUpSubViews
{
//here create your subviews hierarchy
}
Do the following: take the method whichever is the designated initializer for your view. That is, it's the most configureable (i. e. most arguments) init... method that every other initializer calls. For example, it can be -initWithFrame:, as most commonly. Then implement this method as follows:
- (id) initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
// add new views here, for example:
UIImageView *imageView = [[UIImageView alloc] initWithImage:anImage];
[self addSubview:imageView];
[imageView release];
}
return self;
}
Generally , you should create the view and [self addSubView] in viewdidload/viewwillappear of the parent view controller if you want to show the view all the time. You can also set it hidden if you wih. In some other scenario like you want to show a view on a button click or some other actions, you should add subviews accordingly.
By integral, they mean either important or massive. viewDidLoad works just fine, but for really big, or important stuff, initialization methods are the way to go.