How to get rid of the space on the left side of a custom UINavigationItem with a UISearchBar - objective-c

In my navigation bar, I have a magnifying glass icon that brings up a search bar. I'm not using a UISearchDisplayController, so I opted to build my own UINavigationItem and then push it over the standard UINavigationItem using pushNavigationItem.
The problem is that the UINavigationItem seems to be pushed around 8 pixels to the right. This causes the cancel button (with localized text 'Annuleren') to be really close to the edge of the screen.
I tried inspecting the self.mySearchBar.bounds at runtime, but the origin is 0,0. I've played around a bit with AutoLayout and programmatically added constraints, but I haven't been successful. I hope it's possible without AutoLayout.
This is my code:
- (IBAction)displaySearchBar:(id)sender {
if (!self.mySearchNavigationItem)
{
self.mySearchNavigationItem = [[UINavigationItem alloc] initWithTitle:#""];
self.mySearchNavigationItem.hidesBackButton = YES;
self.mySearchBar = [[UISearchBar alloc] initWithFrame:CGRectZero];
self.mySearchBar.showsCancelButton = YES;
self.mySearchBar.delegate = self;
[self.mySearchBar sizeToFit];
[self.mySearchBar setPlaceholder:#"Zoeken..."];
UIView *barWrapper = [[UIView alloc]initWithFrame:self.mySearchBar.bounds];
[barWrapper addSubview:self.mySearchBar];
self.mySearchNavigationItem.leftBarButtonItem = nil;
self.mySearchNavigationItem.backBarButtonItem = nil;
self.mySearchNavigationItem.titleView = barWrapper;
UIButton *cancelButton;
UIView *topView = self.mySearchBar.subviews[0];
for (UIView *subView in topView.subviews) {
if ([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
cancelButton = (UIButton*)subView;
}
}
if (cancelButton) {
[cancelButton setTitle:#"Annuleren" forState:UIControlStateNormal];
}
}
[self.navigationController.navigationBar pushNavigationItem:self.mySearchNavigationItem animated:YES];
NSTimeInterval delay;
if (self.tableView.contentOffset.y >1000) delay = 0.4;
else delay = 0.1;
[self performSelector:#selector(activateSearch) withObject:nil afterDelay:delay];
}

try:
self.navigationController.navigationBar.barTintColor = self.mySearchBar.barTintColor;
if that doesn't work, you can add an underlay view to the navigation controller that is the color you would like. this may be useful: Get the right color in iOS7 translucent navigation bar

After searching for many hours, I gave up and went for a dirty fix. I'll leave it open for a while, in case someone knows why my searchbar is moved 8 pixels to the right.
Right before showing the UINavigationItem, I move the whole UINavigationBar to x-coordinate -8.
self.navigationController.navigationBar.frame = CGRectMake(-8.0, self.navigationController.navigationBar.frame.origin.y, self.navigationController.navigationBar.frame.size.width, self.navigationController.navigationBar.frame.size.height);
[self.navigationController.navigationBar pushNavigationItem:self.mySearchNavigationItem animated:YES];
And then on the cancel button click, I move it back to x-coordinate 0.
- (IBAction)cancelSearchBar:(id)sender {
[self.navigationController.navigationBar popNavigationItemAnimated:YES];
self.navigationController.navigationBar.frame = CGRectMake(0.0, self.navigationController.navigationBar.frame.origin.y, self.navigationController.navigationBar.frame.size.width, self.navigationController.navigationBar.frame.size.height);
}

Related

How to do Card Deck style page transitioning, Vertically & Horizontally, using UIScrollview (or best practice)

I'm creating an app that can slide between pages of a book, left and right (covering or revealing like a card deck), and sliding up and down will either slide down (and cover) a settings page, or slide up (and reveal) to detailed info on the book.
Essentially, its a "card deck" Sliding functionality, similar to this project https://github.com/sweetmandm/CardSliderView-for-iOS which I'm considering using, however I require BOTH vertical AND horizontal "card deck" sliding capability.
To give you another example of the slide/left cover/reveal effect I'm looking, take a look at the new CNN app, where you can slide between articles.
I've also considered using UIPageViewController, however this does not support the "Slide and Reveal / Slide and Cover" transition I'm looking for, rather only a "Slide Over Left or Right" transition, so I would have to hack it somehow and use multiple UIPageViewControllers, one on top of the over, to allow the "reveal and cover" effect to work, using just the gestures from the UIPageViewController to allow the user to swipe.
I'm familiar with the directionalLockEnabled property on UIScrollview, however I'm wondering still what is the overall best approach to get the effect I'm looking for, one that will support both vertical and horizontal, UIScrollView? UIPageViewController? Nested UIScrollviews? Instead of playing around with the directionalLockEnabled property? Something Else?
What would be the best way to achieve the exact user experience I'm looking to provide?
Ok, I found a solution to my own question, using nested UIScrollViews. I also added a very rough solution as project to github: https://github.com/cohen72/InfiniteDeckScroller.
For the horizontal scrolling, I have three horizontal scroll views, one nested into the other. UIScrollview is automatically handling the proper scrolling of each. Depending on the content offset and which scrollview is being scrolled, I know how to "re-arrange/re-order" the nested scrollviews.
Here is a snippet of the solution I came up.
This particular solution allows sliding up to reveal, however I did not yet implement the sliding down to cover, however doing so would use the same approach as the horizontal solution.
#define BOTTOM 1
#define MIDDLE 2
#define TOP 3
#define VERTICAL_SUB 4
- (void)viewDidLoad{
[super viewDidLoad];
UIScrollView *scroll1 = [[UIScrollView alloc] initWithFrame:self.view.bounds];
UIScrollView *scroll2 = [[UIScrollView alloc] initWithFrame:self.view.bounds];
UIScrollView *scroll3 = [[UIScrollView alloc] initWithFrame:self.view.bounds];
UIScrollView *scroll4 = [[UIScrollView alloc] initWithFrame:self.view.bounds];
scroll1.contentSize = CGSizeMake(self.view.frame.size.width * 2, self.view.frame.size.height * 1);
scroll1.tag = BOTTOM;
scroll1.pagingEnabled = YES;
scroll1.bounces = NO;
scroll1.delegate = self;
[scroll1 addSubview:[self labelForScrollView:scroll1 withBgColor:[UIColor redColor]]];
[scroll1 setContentOffset:CGPointMake(0, 0)];
scroll2.contentSize = CGSizeMake(self.view.frame.size.width * 2, self.view.frame.size.height * 1);
scroll2.tag = MIDDLE;
scroll2.pagingEnabled = YES;
scroll2.bounces = NO;
scroll2.delegate = self;
[scroll2 addSubview:[self labelForScrollView:scroll2 withBgColor:[UIColor orangeColor]]];
[scroll2 setContentOffset:CGPointMake(0, 0)];
scroll3.contentSize = CGSizeMake(self.view.frame.size.width * 2, self.view.frame.size.height * 1);
scroll3.tag = TOP;
scroll3.pagingEnabled = YES;
scroll3.bounces = NO;
scroll3.delegate = self;
[scroll3 addSubview:[self labelForScrollView:scroll3 withBgColor:[UIColor yellowColor]]];
[scroll3 setContentOffset:CGPointMake(320, 0)];
scroll4.contentSize = CGSizeMake(self.view.frame.size.width * 1, self.view.frame.size.height * 2);
scroll4.delegate = self;
scroll4.bounces = NO;
scroll4.pagingEnabled = YES;
scroll4.alwaysBounceVertical = NO;
scroll4.tag = VERTICAL_SUB;
[scroll4 addSubview:scroll1];
[scroll1 addSubview:scroll2];
[scroll2 addSubview:scroll3];
[self.view addSubview:scroll4];
}
- (UILabel*)labelForScrollView:(UIScrollView*)scrollView withBgColor:(UIColor*)color{
UILabel *lbl = [[UILabel alloc] initWithFrame:scrollView.bounds];
lbl.textAlignment = NSTextAlignmentCenter;
lbl.backgroundColor = color;
lbl.text = [NSString stringWithFormat:#"ScrollView: %d", scrollView.tag];
return lbl;
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
NSLog(#"content offset: %f, tag: %d ", scrollView.contentOffset.x, scrollView.tag);
UIScrollView *newMiddleScrollView, *newBottomScrollView, *newTopScrollView;
// swipe left
if (scrollView.contentOffset.x == 0 && scrollView.tag == TOP) {
newMiddleScrollView = (UIScrollView*)[self.view viewWithTag:TOP];
newTopScrollView = (UIScrollView*)[self.view viewWithTag:BOTTOM];
newBottomScrollView = (UIScrollView*)[self.view viewWithTag:MIDDLE];
}
// swipe right
else if (scrollView.contentOffset.x == 320 && scrollView.tag == MIDDLE) {
newMiddleScrollView = (UIScrollView*)[self.view viewWithTag:BOTTOM];
newTopScrollView = (UIScrollView*)[self.view viewWithTag:MIDDLE];
newBottomScrollView = (UIScrollView*)[self.view viewWithTag:TOP];
}
else {
return;
}
newMiddleScrollView.tag = MIDDLE;
newBottomScrollView.tag = BOTTOM;
newTopScrollView.tag = TOP;
newBottomScrollView.contentOffset = CGPointMake(0, 0);
newMiddleScrollView.contentOffset = CGPointMake(0, 0);
newTopScrollView.contentOffset = CGPointMake(320, 0);
UIScrollView *verticalScrollView_sub = (UIScrollView*)[self.view viewWithTag:VERTICAL_SUB];
[verticalScrollView_sub addSubview:newBottomScrollView];
[newBottomScrollView addSubview:newMiddleScrollView];
[newMiddleScrollView addSubview:newTopScrollView];
}
That's a good question - correct me if I'm wrong here, but it sounds that from left to right there's an arbitrary number of pages/cards, but only a few standard cards to come in from the top or bottom (your settings and details panels).
If that's the case, then you may well want to stick with something along the lines of a UIPageController alongside some gesture recognisers. You set up your page controller (or whatever controller you end up using to achieve your desired cards animation), and then add two gesture recognizers for swipe up and swipe down.
You can then animate your details/settings views in when you receive those gestures, giving you the card like interface without needing to bother with multiple UIPageViewControllers or a scroll view.
This approach isn't so great if you want an arbitrary number of cards in both the horizontal and vertical, but it sounds as if that's not the case.
There would be a lot of different ways to accomplish this, depending on how you want your different controllers to relate to each other. The slide transition itself is pretty simple. I've implemented it this way in a controller that's the superclass of the controller that calls the method:
-(void)SlideInController:(RDSlideController *) next {
next.presentingVC = self;
next.view.frame = CGRectMake(self.view.frame.origin.x + 320, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
[self.view.window addSubview:next.view];
[UIView animateWithDuration:1 animations:^{
next.view.frame = self.view.frame;
} completion:^(BOOL finished) {
self.view.window.rootViewController = next;
}];
}
Then from the newly presented controller, you can call this method to go back:
-(void)SlideOut {
UIViewController *prev = self.presentingVC;
prev.view.frame = self.view.frame;
[self.view.window insertSubview:prev.view belowSubview:self.view];
[UIView animateWithDuration:1 animations:^{
self.view.frame = CGRectMake(self.view.frame.origin.x + 320, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
} completion:^(BOOL finished) {
self.view.window.rootViewController = prev;
}];
}

Pop up tutorial page like Photosynth's

I want to create something that is basically a clone of what photosynth does for their tutorial page. A small "?" button pops up what looks like a new view in a frame that is slightly smaller than the first view, so that you can still see the first view around the edges.
It's a little tough to see from the pic above, but the part around the edges is the old view that the tutorial display popped up over.
My first guess is that I need to use a container view somehow, but I can't find anything on the web about exactly how to do this. I can currently create a container view, hook it up to a new view controller via a segue, and do whatever I want in that new view controller, but the container view is always visible on the view it is contained within. Any help?
BTW, I'm using storyboarding with ARC.
You can add a transparent view to the key window, add a tap gesture recognizer that would dismiss it and the subviews to show the content:
#define OVERLAY_TAG 997
-(void)showTutorial
{
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *overlay = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
overlay.backgroundColor = [UIColor clearColor];
overlay.userInteractionEnabled = YES;
[keyWindow addSubview:overlay];
UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(dismissTutorial)];
CGFloat border = 10;
CGRect frame = overlay.bounds;
// 20 is the status bar height (sorry for using the number)
frame = CGRectMake(border, border + 20, frame.size.width - border * 2, frame.size.height - border * 2 - 20);
// the black view in the example is probably a scroll view
UIView *blackView = [[UIView alloc] initWithFrame:frame];
blackView.backgroundColor = [UIColor blackColor];
blackView.alpha = 0.0;
[overlay addSubview:dimView];
// add all the subviews for your tutorial
// make it appear with an animation
[UIView animateWithDuration:0.3
animations:^{dimView.alpha = 1;}
completion:^(BOOL finished){[overlay addGestureRecognizer:tapRecognizer];}];
}
-(void)dismissTutorial
{
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *overlay = [keyWindow viewWithTag:OVERLAY_TAG];
[UIView animateWithDuration:0.3
animations:^{
overlay.alpha = 0.0;
}
completion:^(BOOL finished){
[overlay removeFromSuperview];
}];
}
This way you would remove the tutorial with a simple tap but you can use a button for instance.

Present formsheet modal ViewController using horizontal flip

I currently show a modal UIViewController in the formSheet style that was presented with the coverVertical animation. I am trying to present another modal UIViewController from it, using currentContext for the presentation style and flipHorizontal for the animation style. It does work, but there is a solid white white background behind the flip as it occurs. Any advice on how to successfully present another formsheet modal UIViewController using the flipHorizontal style from a pre-existing modal UIViewController in the formSheet style would be appreciated please! Thanks
Code:
// Loading of first modalVC
- (void)showModalVC {
Modal1VC *modal1VC = [[Modal1VC alloc] init];
modal1VC.modalPresentationStyle = UIModalPresentationFormSheet;
modal1VC.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:modal1VC animated:YES];
[modal1VC release];
modal1VC.view.superview.bounds = CGRectMake(0, 0, 400, 400);
}
// Loading of second modalVC in Modal1VC
- (void)buttonTapped:(id)sender {
UIViewController *modal2VC = [[UIViewController alloc] init];
modal2VC.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
modal2VC.modalPresentationStyle = UIModalPresentationCurrentContext;
modal2VC.view.backgroundColor = [UIColor greenColor];
[self presentModalViewController:modal2VC animated:YES];
[modal2VC release];
modal2VC.view.superview.bounds = CGRectMake(0, 0, 400, 400);
}
UIViewController *presentingViewController = //allocate your VC
[modalViewController setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
UIViewController *modalViewController = //your modal VC
[presentingViewController presentModalViewController:modalViewController animated:YES];
This is all the code you need ;)
It sounds like your transition is working, but the white background shown on the flip transition is the problem?
During the flip transition, the white background that is shown is actually the window. You can change the color that is shown by setting the backgroundColor property of window in the AppDelegate.
That would seem to be a limitation of presentModalViewController:animated:.
Instead of [self presentModalViewController:modal2VC animated:YES], you may be able to get the effect you want using UIView animations. Perhaps something like this:
[UIView transitionWithView:self.view.window
duration:0.3
options:UIViewAnimationOptionTransitionFlipFromRight
animations:^{
[self presentModalViewController:modal2VC animated:NO];
} completion:NULL];

detect long press on UINavigationItem's back button

I want to add functionality to my back buttons through my UINavigationController-based app where long-pressing the back button will pop to root. However, I can't figure out where to attach the gesture recognizer. Do I subclass UINavigationBar and try and detect if the long press is in the left button region?
I've heard of people adding similar functionality before. Anyone have any ideas?
I know this question is old, but I came up with a solution. Instead of trying to add the gesture recognizer to the button itself (which would be ideal), I added it to the self.navigationController.navigationBar and then in the action method, use the locationInView to see if I'm over the back button. I wasn't entirely sure about how to identify the back button precisely, so I'm clumsily just grabbing the the first subview with an x coordinate less than some arbitrary value, but it seems promising. If someone has a better way to identify the frame of the back button, let me know.
- (void)longPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
// set a default rectangle in case we don't find the back button for some reason
CGRect rect = CGRectMake(0, 0, 100, 40);
// iterate through the subviews looking for something that looks like it might be the right location to be the back button
for (UIView *subview in self.navigationController.navigationBar.subviews)
{
if (subview.frame.origin.x < 30)
{
rect = subview.frame;
break;
}
}
// ok, let's get the point of the long press
CGPoint longPressPoint = [sender locationInView:self.navigationController.navigationBar];
// if the long press point in the rectangle then do whatever
if (CGRectContainsPoint(rect, longPressPoint))
[self doWhatever];
}
}
- (void)addLongPressGesture
{
if (NSClassFromString(#"UILongPressGestureRecognizer"))
{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[self.navigationController.navigationBar addGestureRecognizer:longPress];
[longPress release];
}
}
I believe UIGestureRecognizers can only be added to UIViews and subclasses of UIViews.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html
The back button is a UIBarButtonItem that descends from NSObject. Therefore, you won't be able to attach a gesture recognizer to a standard back button using
UILongPressGestureRecognizer *longPressGesture =
[[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(longPress:)] autorelease];
[self.navigationItem.backBarButtonItem addGestureRecognizer:longPressGesture];
You can however add a custom view to a UIBarButtonItem. A custom view could just as easily be a UIView, UIButton, UILabel, etc.
Example:
UIView *myTransparentGestureView = [[UIView alloc] initWithFrame:CGRectMake(0,0,40,30)];
[myTransparentGestureView addGestureRecognizer:longPressGesture];
[self.navigationItem.backBarButtonItem setCustomView:myTransparentGestureView];
// Or you could set it like this
// self.navigationItem.backBarButtonItem.customView = myTransparentGestureView;
[myTransparentGestureView release];
You have to be careful however, since setting properties on backBarButtonItem applies to the next view that you push. So if you have view A that pushes to view B and you want the gesture to be recognized when you tap back in view B. You must set it up in view A.
I followed a slightly different path, figured I'd share it. The above answers are fine, but really, if the long press is in the leading 1/3 of the nav bar, that's good enough for me:
- (void)longPress:(UILongPressGestureRecognizer *)gr
{
NSLog(#"longPress:");
UINavigationBar *navBar = [self navigationBar];
CGFloat height = navBar.bounds.size.height;
CGPoint pt = [gr locationOfTouch:0 inView:navBar];
//NSLog(#"PT=%# height=%f", NSStringFromCGPoint(pt), height);
if(CGRectContainsPoint(CGRectMake(0,0,100,height), pt)) {
[self popToViewController:self.viewControllers[0] animated:YES];
}
}
Here's my solution:
In appDelegate (the "owner" of the nav bar in my app), In applicationDidFinishLaunchingWithOptions:
Get the nav bar view and add the gesture recognizer to the whole view:
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(backButtonLongPress:)];
[myNavBar addGestureRecognizer:longPress];
NSLog(#"Gesture Recognizer Added.");
Then in appDelegate, in -(void) backButtonLongPress:(id) sender
Check to see if the gesture occurs within the frame of the back button:
if ([sender state] == UIGestureRecognizerStateBegan) {
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
// Get the back button view
UIView *backButtonView = nil;
for (UIView *view in [myNavBar subviews]) {
if ([[[view class] description] isEqualToString:#"UINavigationItemButtonView"]) {
backButtonView = view;
NSLog(#"Found It: %#", backButtonView);
NSLog(#"Back Button View Frame: %f, %f; %f, %f", backButtonView.frame.origin.x, backButtonView.frame.origin.y, backButtonView.frame.size.width, backButtonView.frame.size.height);
}
}
CGPoint longPressPoint = [sender locationInView:myNavBar];
NSLog(#"Touch is in back button: %#", CGRectContainsPoint(backButtonView.frame, longPressPoint) ? #"YES" : #"NO");
if (CGRectContainsPoint(backButtonView.frame, longPressPoint)) {
// Place your action here
}
// Do nothing if outside the back button frame
}

Objective C: Using code to add a toolbar to a UITableView (within a Navigation Controller)

I managed to add a toolbar at the bottom of my UITableView using the code below:
toolbar = [[UIToolbar alloc] init];
toolbar.barStyle = UIBarStyleDefault;
toolbar.frame = CGRectMake(0, 436, 320, 50);
//Set the toolbar to fit the width of the app.
[toolbar sizeToFit];
[self.navigationController.view addSubview:toolbar];
However when I try to switch back to the first page of the navigation controller, the toolbar at the bottom of the page is still displayed. How can I ensure that the toolbar is only shown on the UITable View and not any other views in the navigation controller?
Thanks in advance.
Zhen
In your TableViewController implement:
- (void)viewWillAppear:(BOOL)animated
{
self.navigationController.toolbar.hidden = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
self.navigationController.toolbar.hidden = YES;
}