Reusable button bars? - objective-c

I'm implementing an IOS6 app using storyboards. I want every screen--excuse me, scene--for the app to have a view at the top containing different image buttons of different sizes. Tapping the buttons takes the user to different scenes of the app.
That's too complex for a UITabController, as far as I can tell. I tried making a separate view controller for that view and including the view in each scene, but any functionality in the view--such as the buttons--causes the app to crash.
It looks like I may have to implement this view in the storyboard in one scene, then copy and paste it into every other scene, wiring up the segues from every scene to every other scene. What a maintenance nightmare! Is there a better way?

Since you are trying to create a custom UITabBarController, you should use a container view controller. To do that:
Open your storyboard and add a custom UIVIewController (let's call it ContainerViewController).
Insert the UIVIews that represent your tabs into that controller and then insert another UIVIew (*currentView in the code below) that will take the rest of the screen. That's what the child controllers will use to display their scene.
Create a UIVIewController for each scene (child controller) you need, as you would normally, and give each of them a unique identifier (Identity Inspector -> Storyboard ID)
Now you have to add the following code your ContainerViewController:
#interface ContainerViewController ()
#property (strong, nonatomic) IBOutlet UIView *currentView; // Connect the UIView to this outlet
#property (strong, nonatomic) UIViewController *currentViewController;
#property (nonatomic) NSInteger index;
#end
#implementation ContainerViewController
// This is the method that will change the active view controller and the view that is shown
- (void)changeToControllerWithIndex:(NSInteger)index
{
if (self.index != index){
self.index = index;
[self setupTabForIndex:index];
// The code below will properly remove the the child view controller that is
// currently being shown to the user and insert the new child view controller.
UIViewController *vc = [self setupViewControllerForIndex:index];
[self addChildViewController:vc];
[vc didMoveToParentViewController:self];
if (self.currentViewController){
[self.currentViewController willMoveToParentViewController:nil];
[self transitionFromViewController:self.currentViewController toViewController:vc duration:0 options:UIViewAnimationOptionTransitionNone animations:^{
[self.currentViewController.view removeFromSuperview];
[self.currentView addSubview:vc.view];
} completion:^(BOOL finished) {
[self.currentViewController removeFromParentViewController];
self.currentViewController = vc;
}];
} else {
[self.currentView addSubview:vc.view];
self.currentViewController = vc;
}
}
}
// This is where you instantiate each child controller and setup anything you need on them, like delegates and public properties.
- (UIViewController *)setupViewControllerForIndex:(NSInteger)index {
// Replace UIVIewController with your custom classes
if (index == 0){
UIViewController *child = [self.storyboard instantiateViewControllerWithIdentifier:#"STORYBOARD_ID_1"];
return child;
} else {
UIViewController *child = [self.storyboard instantiateViewControllerWithIdentifier:#"STORYBOARD_ID_2"];
return child;
}
}
// Use this method to change anything you need on the tabs, like making the active tab a different colour
- (void)setupTabForIndex:(NSInteger)index{
}
// This will recognize taps on the tabs so the change can be done
- (IBAction)tapDetected:(UITapGestureRecognizer *)gestureRecognizer {
[self changeToControllerWithIndex:gestureRecognizer.view.tag];
}
Finally, each view you create that represents a tab should have it's own TapGestureRecognizer and a number for its tag.
By doing all this you will have a single controller with the buttons you need (they don't have to be reusable), you can add as much functionality you want in them (that's what the setupTabBarForIndex: method will be used) and you won't violate DRY.

Related

Auto layout on subview inside subview (NSView)

I'm having hard time trying to properly set the auto layout for a subview inside another subview.
I'm using an example where two toolbar items show two different subviews (which works as expected), and those two share a third subview that's the one that does not fit well.
The code to add the subview is very simple:
[subView removeFromSuperview];
[itemXSubView addSubview:subView];
[self.window setContentView:itemXView];
First I remove the third and shared subview (subView) in case it was already added, then add it to the item[1-2]SubView and set the content of the window with the subview item[1-2]View, [1-2] depending on the toolbar button selected. Everything else is done with auto layout conditions.
The result is that the third and shared subview is always misplaced and/or cut, as in the example below. Resizing the window and changing from the first or the second view usually aggravates the issue.
Example of third subview items cut
Test updates
Tried to delegate the main window and override two resize functions (as per #the4kman suggestions), but they did never get called. The init is the only being called:
#interface viewController: NSView <NSWindowDelegate>
#end
#implementation viewController
-(id)init
{
if((self=[super init])) { }
return self;
}
- (void)resizeSubviewsWithOldSize:(NSSize)oldSize;
{
[super resizeSubviewsWithOldSize:oldSize];
}
- (void)resizeWithOldSuperviewSize:(NSSize)oldSize;
{
[super resizeWithOldSuperviewSize:oldSize];
}
- (void)layout
{
[super layout];
}
Another suggestion that got called, but sadly with no actual improvement. Delegated the window to viewController and set the main view (self.view) to the nested subView. Tried also combining with [itemXSubView setNeedsLayout:true];:
#interface viewController: NSViewController <NSWindowDelegate>
#end
#implementation viewController
-(void)viewWillLayout
{
[super viewDidLayout];
[self.view setNeedsLayout:true];
}
#end
Thanks in advance!
itemXSubView resizes its subviews but it doesn't fit them. You have to fit subView inside item1SubView before addSubview.
- (IBAction)item1Action:(id)sender {
NSLog(#"Item 1 action triggered");
[subView removeFromSuperview];
subView.frame = item1SubView.bounds;
[item1SubView addSubview:subView];
[self.window setContentView:item1View];
}

How do you specify the origin of the arrow on a popover segue with OS X 10.10 storyboards

I'm playing around with storyboarding in an OS X 10.10 app. I have an NSTableView that, when you click a specific row opens a segue that goes to a popover that contains an NSViewController.
How do you specify the origin NSPoint of the arrow for the popover? Right now, it just points to the NSTableView in the middle. I assumed that I could do this in prepareForSegue, but I can't seem to figure it out. prepareForSegue doesn't seem to have an understanding that the NSViewController is contained in an NSPopover
Any ideas?
You should file an enhancement request Radar for this behavior if you think it should be provided by the framework in some way.
But to workaround this in the meantime, you can create your own custom NSStoryboardSegue subclass to help with this.
#interface TablePopoverSegue : NSStoryboardSegue
#property (weak) NSTableView *anchorTableView;
#property NSRectEdge preferredEdge;
#property NSPopoverBehavior popoverBehavior;
#end
#implementation TablePopoverSegue
- (void)perform {
if ([self anchorTableView]) {
NSInteger selectedColumn = [[self anchorTableView] selectedColumn];
NSInteger selectedRow = [[self anchorTableView] selectedRow];
// If we can pick a specific row to show from, do that; otherwise just fallback to showing from the tableView
NSView *anchorView = [self anchorTableView];
if (selectedRow >= 0) {
anchorView = [[self anchorTableView] viewAtColumn:selectedColumn row:selectedRow makeIfNecessary:NO];
}
// Use the presentation API so that the popover can be dismissed using -dismissController:.
[[self sourceController] presentViewController:[self destinationController] asPopoverRelativeToRect:[anchorView bounds] ofView:anchorView preferredEdge:[self preferredEdge] behavior:[self popoverBehavior]];
}
}
#end
This can be specified in IB in the inspector panel for the segue (just like iOS):
And then in your source view controller's prepareForSegue:, you can just set up the segue:
- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
if ([segue isKindOfClass:[TablePopoverSegue class]]) {
TablePopoverSegue *popoverSegue = (TablePopoverSegue *)segue;
popoverSegue.preferredEdge = NSMaxXEdge;
popoverSegue.popoverBehavior = NSPopoverBehaviorTransient;
popoverSegue.anchorTableView = [self tableView];
}
}

Automatically click segmentControl with index 0 on ViewDidAppear

I am hoping to "automate" a click on the segmentController with the index of 0.
My tabBar-based app has multiple segmentControllers in a tab in the ViewDidAppear method, I would like to automatically have it "click" the first segmented controller.
if (segmentController.selectedSegmentIndex == 0) {
//stuff here
}
if (segmentController.selectedSegmentIndex == 1) {
//stuff here
}
Does anyone know how I might accomplish this? Thank you!
If you're creating it programmatically, you could lazy load it like this:
#interface ExampleViewController : UIViewController
#property (nonatomic, strong) UISegmentedControl *segmentedControl;
- (void)segmentedControlClicked:(UISegmentedControl *)segmentedControl;
#end
#implementation ExampleViewController
- (UISegmentedControl *)segmentedControl
{
if (!_segmentedControl)
{
NSArray *items = #[#"First", #"Second", #"Third"];
_segmentedControl = [[UISegmentedControl alloc] initWithItems:items];
[_segmentedControl addTarget:self
action:#selector(segmentedControlClicked:)
forControlEvents:UIControlEventValueChanged];
[_segmentedControl setSelectedSegmentIndex:0]; // Set Default selection
CGRect frame = _segmentedControl.frame;
frame.origin = CGPointMake(0.0f, 0.0f); // Move to wherever you need it
[self.view addSubview:_segmentedControl];
}
return _segmentedControl;
}
- (void)segmentedControlClicked:(UISegmentedControl *)segmentedControl
{
// Whatever your code is goes here...
}
#end
If you're wanting a method to be called also initially, you can call it within your viewDidLoad: method as such:
- (void)viewDidLoad
{
[self.segmentedControl setSelectedSegmentIndex:0]; // Set desired default index (optional if set in lazy load as shown above)
[self segmentedControlClicked:self.segmentedControl];
}
This would hence simulate a click on desired default index.
Be careful putting the above into viewDidAppear: (you could if you really wanted to) because anytime the view comes to the front, this method will be called (in example, if this view controller presents a modal view controller, once the modal is dismissed, this view controller's viewDidAppear: method will be called).
Cheers!
Set the selectedSegmentIndex property on your UISegmentedControl in your viewDidAppear (or viewDidLoad) method.
self.segmentedController.selectedSegemntIndex = 1;
UISegmentedControl Reference

Reversal of Custom Segue with Storyboarding

I have a custom segue animation that occurs when pushing a new view controller onto the stack. When popping the view controller that was presented with said custom segue, however, the default navigation controller animation happens (that is, the current view controller animates to the right while the parent view controller translates on-screen from the left edge).
So my question is this: is there a way to write a custom pop segue animation which happens when popping a view controller off the stack?
Edit (solution):
I ended up defining a custom segue similar to the selected answer. In the Storyboard, I dragged a custom segue from the child view controller back to its parent, gave it an identifier and the newly written reverse segue as its class. Yes, I realize it is virtually identical to a modal transition. Client requirements necessitated this madness, so before anyone comments, understand that I know one shouldn't have to do this under normal circumstances.
- (void)perform {
UIViewController *src = (UIViewController *)self.sourceViewController;
UIViewController *dest = (UIViewController *)self.destinationViewController;
[UIView animateWithDuration:0.3 animations:^{
CGRect f = src.view.frame;
f.origin.y = f.size.height;
src.view.frame = f;
} completion:^(BOOL finished){
src.view.alpha = 0;
[src.navigationController popViewControllerAnimated:NO];
}];
}
Yes. Here is an example where I pop to the top level. When your create the segue in Storyboard. Use select or enter the new new segue class in the attributes inspector.
//
// FlipTopPop.h
#import <UIKit/UIKit.h>
#interface FlipTopPopToRoot : UIStoryboardSegue
#end
and
// FlipTopPop.m
#import "FlipTopPopToRoot.h"
#implementation FlipTopPopToRoot
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
[UIView transitionWithView:src.navigationController.view duration:0.5
options:UIViewAnimationOptionTransitionFlipFromBottom
animations:^{
[src.navigationController popToViewController:[src.navigationController.viewControllers objectAtIndex:0] animated:NO];;
}
completion:NULL];
}
#end
If you want to pop up just one level change use this custom segue:
// PopSegue.h
#import <UIKit/UIKit.h>
#interface PopSegue : UIStoryboardSegue
#end
and
// PopSegue.m
#import "PopSegue.h"
#implementation PopSegue
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
[src.navigationController popViewControllerAnimated:YES];
}
#end
For anyone following this now, iOS 7 lets you animate both ways:
Set the segue to Push, then see code below for a push implementation.
https://github.com/Dzamir/OldStyleNavigationControllerAnimatedTransition
As the commenter Linus pointed out, the other solutions presented will create another instance of the UIViewController. I think this link here describe other alternatives to enabling reverse segue animations.
http://robsprogramknowledge.blogspot.com/2012/05/back-segues.html

firstResponder in NSViewController

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