TvOS UITabBarController detect tabbar shown/hidden - objective-c

Is there anyway to detect that the tabbar of a UITabBarController is going to appear or disappear? I want to make an animation simultaneously with the animation that shows/hides the tabbar.
I haven't find any way to detect this event. The property "hidden" of the tabbar is not an option because it changes its value once the animation has finished

The solution was to use the method in the view controller didUpdateFocusInContext:withAnimationCoordinator: with this code:
static NSString *kUITabBarButtonClassName = #"UITabBarButton";
NSString *prevFocusViewClassName = NSStringFromClass([context.previouslyFocusedView class]);
NSString *nextFocusedView = NSStringFromClass([context.nextFocusedView class]);
// The tabbar is going to disappear
if ([prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
![nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
[self.view layoutIfNeeded];
self.constraintScrollViewCenterY.constant -= self.tabBarController.tabBar.frame.size.height;
[coordinator addCoordinatedAnimations:^{
[self.view layoutIfNeeded];
} completion:nil];
// The tabbar is going to appear
} else if (![prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
[nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
[self.view layoutIfNeeded];
self.constraintScrollViewCenterY.constant += self.tabBarController.tabBar.frame.size.height;
[coordinator addCoordinatedAnimations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
where self.constraintScrollViewCenterY is a constraint related to the vertical alignment of the view I want to move according to the tabbar movement
Note: The use of class name (kUITabBarButtonClassName) instead of [... class] method is due to UITabBarButton is a private class

Related

Manually calling UIRefreshControl and using setContentOffset

I have implemented refreshControl as shown below in viewDidLoad():
refreshControl = [[UIRefreshControl alloc] init];
if (#available(iOS 10.0, *)) {
self.tableView.refreshControl = refreshControl;
} else {
[self.tableView addSubview:refreshControl];
}
Now I am presenting another viewController which has options to select filters. And after selecting those filters you come back again to current viewController having refreshControl.
I have added below code in viewDidAppear() for manually calling beginRefreshing:
if (self.filterChanged) {
self.filterChanged = NO;
[self.activityTableView setContentOffset:CGPointMake(0, - refreshControl.frame.size.height) animated:YES];
[refreshControl setHidden:NO];
[refreshControl beginRefreshing];
}
I have used setContentOffset for scrolling back to top and showing refreshControl.
The only problem is suppose my tableView is half scrolled in between then there is a big gap between refreshControl.
If my tableView is not scrolled then it works fine like I have pulled down to refresh, but if it is half scrolled then inspite of giving setContentOffset there is a big gap between refreshControl and tableview.
You need to wait for the scroll to finish before you can start the refresh. Unfortunately there is no completion block on setContentOffset, so try this.
After creating your refresh control set the target and create a corresponding method.
[_refreshControl addTarget:self action:#selector(refreshRequested) forControlEvents:UIControlEventValueChanged];
- (void)refreshRequested {
[_activityTableView reloadData];
}
In your viewDidAppear you scroll to top and refresh when done.
[UIView animateWithDuration:0.25 delay:0 options:0 animations:^(void){
self.activityTableView.contentOffset = CGPointMake(0, -self.refreshControl.frame.size.height);
} completion:^(BOOL finished){
[self.refreshControl beginRefreshing];
[self refreshRequested];
}];
When finished loading you need to end refresh and make sure to scroll back to zero position.
- (void)finishedLoading {
[_refreshControl endRefreshing];
[self.activityTableView setContentOffset:CGPointMake(0, 0) animated:YES];
}

Does presentViewController automatically destroy previous views?

I use a bunch of custom segues to segue from one view controller view to another using:
[self performSegueWithIdentifier:#"createTeamAccountSegue" sender:self];
My question is, do the previous views automatically get destroyed in iOS5 and 6?
I keep having to create custom segues to go to the next view, and then create yet another new segue animation in reverse to go back to the last view. As long as they dont keep stacking up and up then this should be fine right?
EDIT: I should probably show you the general layout of what my custom UIStoryboardSegue class looks like:
- (void) perform {
UIViewController *sourceViewController = (UIViewController *) self.sourceViewController;
UIViewController *destinationViewController = (UIViewController *) self.destinationViewController;
UIView *parent = sourceViewController.view.superview;
[parent addSubview:destinationViewController.view];
[parent sendSubviewToBack:destinationViewController.view];
sourceViewController.view.layer.masksToBounds = NO;
sourceViewController.view.layer.cornerRadius = 8; // if you like rounded corners
sourceViewController.view.layer.shadowOffset = CGSizeMake(0,0);
sourceViewController.view.layer.shadowRadius = 10;
sourceViewController.view.layer.shadowOpacity = 1;
destinationViewController.view.frame = CGRectMake(0, 20, destinationViewController.view.frame.size.width, destinationViewController.view.frame.size.height);
sourceViewController.view.frame = CGRectMake(0, 20, sourceViewController.view.frame.size.width, sourceViewController.view.frame.size.height);
[UIView animateWithDuration:.3 delay:0.0 options:UIViewAnimationCurveEaseInOut animations:^
{
sourceViewController.view.frame = CGRectMake(0, parent.frame.size.height, sourceViewController.view.frame.size.width, sourceViewController.view.frame.size.height);
} completion:^(BOOL finished)
{
[sourceViewController presentViewController:destinationViewController animated:NO completion:NULL];
}];
}
If you are doing there segues in a UINavigationController then no, they aren't destroyed. To go back to one you should use [self.navigationController popViewControllerAnimated:YES];

Subview has wrong orientation

I have a custom segue where I am trying to do the reverse of the standard "Cover Vertical" segue transition. I think this should work:
UIView *srcView = ((UIViewController *)self.sourceViewController).view;
UIView *dstView = ((UIViewController *)self.destinationViewController).view;
[dstView setFrame:CGRectMake(0, 0, srcView.window.bounds.size.width, srcView.window.bounds.size.height)];
[srcView.window insertSubview:dstView belowSubview:srcView];
[UIView animateWithDuration:0.3
animations:^{
srcView.center = CGPointMake(srcView.center.x - srcView.frame.size.width, srcView.center.y);
}
completion:^(BOOL finished){
[srcView removeFromSuperview];
}
];
The problem is the destination view shows up in portrait orientation even though every other view in the app is landscape orientation. Also, the x and y coordinates are reversed. Why is this happening?
I have set up shouldAutorotateToInterfaceOrientation in every view controller, but that didn't help. I could rotate the view with CGAffineTransformMakeRotation, but that doesn't seem like the right thing to do; coordinates would still be reversed, for one thing.
Update:
I tried using CGAffineTransformRotate to rotate the view , but it looks ridiculous. Most of the background shows up black. I don't think this is the way it's suppose to work.
Since I was not able the get the correct orientation and framing of the destination view before the controllers exchange, I did reverse the process:
save the source view as an image (see Remove custom UIStoryboardSegue with custom animation ) ;
switch the current controller [presentViewController: ...];
then, do the animation with the (now correct) destination view embedding the saved source image.
Here is the complete code :
#include <QuartzCore/QuartzCore.h>
#implementation HorizontalSwipingSegue
- (void) perform {
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
UIView *sourceView = source.view;
UIView *destinationView = destination.view;
// Create a UIImageView with the contents of the source
UIGraphicsBeginImageContext(sourceView.bounds.size);
[sourceView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *sourceImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *sourceImageView = [[UIImageView alloc] initWithImage:sourceImage];
[[self sourceViewController] presentViewController:[self destinationViewController] animated:NO completion:NULL];
CGPoint destinationViewFinalCenter = CGPointMake(destinationView.center.x, destinationView.center.y);
CGFloat deltaX = destinationView.frame.size.width;
CGFloat deltaY = destinationView.frame.size.height;
switch (((UIViewController *)self.sourceViewController).interfaceOrientation) {
case UIDeviceOrientationPortrait:
destinationView.center = CGPointMake(destinationView.center.x - deltaX, destinationView.center.y);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaX, sourceImageView.center.y);
break;
case UIDeviceOrientationPortraitUpsideDown:
destinationView.center = CGPointMake(destinationView.center.x + deltaX, destinationView.center.y);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaX, sourceImageView.center.y);
break;
case UIDeviceOrientationLandscapeLeft:
destinationView.center = CGPointMake(destinationView.center.x, destinationView.center.y - deltaY);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaY, sourceImageView.center.y);
break;
case UIDeviceOrientationLandscapeRight:
destinationView.center = CGPointMake(destinationView.center.x, destinationView.center.y + deltaY);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaY, sourceImageView.center.y);
break;
}
[destinationView addSubview:sourceImageView];
[UIView animateWithDuration:0.6f
animations:^{
destinationView.center = destinationViewFinalCenter;
}
completion:^(BOOL finished){
[sourceImageView removeFromSuperview];
}];
}
#end
UPDATED ANSWER:
Make sure you are setting your dstView's frame correctly. I think you'll have better luck using:
dstView.frame = srcView.frame;
instead of:
[dstView setFrame:CGRectMake(0, 0, srcView.window.bounds.size.width, srcView.window.bounds.size.height)];
the bounds property does not react to changes in orientation the same way frame does.
Apparently, it was much easier than I thought to do the segue I wanted. Here's is what worked:
- (void)perform {
UIViewController *src = (UIViewController *)self.sourceViewController;
[src dismissViewControllerAnimated:YES completion:^(void){ [src.view removeFromSuperview]; }];
}
Hopefully this helps someone else.
Replace
[srcView insertSubview:dstView belowSubview:srcView];
with
[srcView insertSubview:dstView atIndex:0];
If you are just adding another view controller's view to your view hierarchy, that view controller is not going to get any information about the orientation of the device, so it will present the view in its default configuration. You need to add the view controller into the view controller hierarchy as well, using the containment methods described here. You also need to remove it from the hierarchy when done.
There are also several good WWDC talks on the subject, one from 2011 called "Implementing view controller containment" and one from this year, the evolution of view controllers.

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
}