Animate UINavigationBar when pushing view controller onto stack - uiviewcontroller

The Apple Music app has a nice transition when a view controller with a visible navigation bar ("My Music" view controller) has a view controller with a transparent navigation bar (An artist's view controller) pushed on to the stack.
I'm looking to recreate this transition myself.
I've set the navigationbar to be transparent with the following code:
private func _setNavigationBarVisible(isVisible isVisible: Bool)
{
title = nil
/*
Update the navigation bar's visibility.
Create a helper method to prevent running the same code twice.
*/
func _updateNavigationBarVisibility(isVisible isVisible: Bool, userViewController: UserViewController?)
{
// Create a dummy navigation bar we can rip the default values out of if it should be visible
let navigationBar = UINavigationBar()
let backgroundImage = isVisible ? navigationBar.backgroundImageForBarMetrics(.Default) : UIImage()
let shadowImage = isVisible ? navigationBar.shadowImage : UIImage()
userViewController?.navigationController?.navigationBar.setBackgroundImage(backgroundImage,
forBarMetrics: .Default)
userViewController?.navigationController?.navigationBar.shadowImage = shadowImage
}
// Animate alongside the view controller's presentation transition if there is one
let isTransitionAnimationRun = transitionCoordinator()?.animateAlongsideTransition({ [weak self] context in
_updateNavigationBarVisibility(isVisible: isVisible, userViewController: self)
}, completion: nil)
// Or just update the values if there's no transition
if isTransitionAnimationRun == false
{
_updateNavigationBarVisibility(isVisible: isVisible, userViewController: self)
}
}
This function is run in my viewWillAppear: and viewWillDisappear: methods passing false and true to the isVisible parameter respectively.
This code does achieve a similar effect, but despite the code doing what it should, I want a different effect.
Currently, this code animates the shadow image making them visible on both view controllers rather than like Apple Music where only the one that presented it should have the shadow. As for the background image, there's a visible "swipe" animation as the navigation bar's background animates from transparent to translucent.
There's also an issue with the swipe gesture which I am looking in to, but currently if during the swipe gesture you keep your finger in one position so no swiping occurs and then lift the finger, the navigationbar updates as though the swipe finished, but the view controllers become glitched and the stack is never pushed or popped despite using the fully useable navigation bar.

Related

Why doesn't UINavigationController change orientation when I push a controller with different supported orientations?

How my code is set up:
I use a navigation controller, with a delegate that controls the orientation:
class NavDelegate: UINavigationControllerDelegate {
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
print("Checking orientation")
if topViewController != nil {
return topViewController!.supportedInterfaceOrientations
}
return .portrait
}
...
}
The main view controller of my app is named MainController, and it's portrait-only:
class MainController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
...
}
I have another controller PhotoController, which supports all four orientations:
class PhotoController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .all
}
...
}
I push the PhotoController on top of the MainController, using pushViewController().
The problem:
Each controller by itself handles orientations correctly — MainController stays in portrait as I rotate the phone to all four orientations, and PhotoController rotates to all four orientations.
But this scenario doesn't work correctly:
Put the phone in landscape, with main controller on screen.
The controller is in portait (as expected).
Push the PhotoController on the stack.
I expect the PhotoController to open in landscape, based on the device orientation, but it remains in portrait. If I manually rotate the phone back and forth, the controller rotates with the phone. Obviously, the user should never have to do this, rotate the phone back and forth to get the controller to rotate.
When I tap the button in MainController that pushes PhotoController, navigationControllerSupportedInterfaceOrientations() isn't invoked.
What else I tried:
Overriding shouldAutorotate to return true in PhotoController.
... and in MainController.
Overriding supportedInterfaceOrientations in UINavigationController instead of using the delegate.
Checking UINavigationController.visibleViewController instead of .topViewController. This didn't work because the topviewController turns out to be an iOS private class UISnapshotModalViewController, not my class, so calling supportedInterfaceOrientation() on it presumably doesn't return the right value.
None of these work.
The solution turned out to be to invoke UIViewController. attemptRotationToDeviceOrientation() in viewWillAppear() or viewDidAppear().
In the former case, the animation of pushing the controller occurs concurrently with the device rotation animation, which looks odd. In the latter case, the rotation happens after the controller transition animation completes. Which also looks odd, though in a different way.

tvOS - catch MENU button presses during push-view-controller transition

I have a UIViewController in my tvOS app which will only appear for a few seconds, and needs totally customizable MENU button handling. I create it like so:
- (void)viewDidLoad
{
[super viewDidLoad];
// Add a tap gesture recognizer to handle MENU presses.
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
tapGestureRec.allowedPressTypes = #[#(UIPressTypeMenu)];
[self.view addGestureRecognizer:tapGestureRec];
}
- (void)handleTap:(UITapGestureRecognizer *)sender
{
// Code to process the MENU button is here.
}
I display the view controller using pushViewController:animated::
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:identifier];
[self pushViewController:controller animated:isAnimated];
I've found that if the user presses MENU as soon as the screen starts to appear, while the cross-fade transition effect is still displaying, they are able to dodge the UITapGestureRecognizer and go back to a previous screen, which is not intended. They can also cause issues by mashing MENU over and over again--eventually they will escape out of things that they should not be able to.
How can I ensure that the MENU press always reaches my override? Is there a way to specify an app-encompassing MENU button handler and still use a UINavigationController?
A solution that worked for me was to install a UITapGestureRecognizer onto self.navigationController.view. Any taps which get missed by the regular UIViewControllers end up getting caught by the navigation controller's recognizer instead. If you install UITapGestureRecognizers on each view controller you create, the only taps which will fall through the cracks to the navigation controller are taps that occur mid-transition, and it is safe to just ignore those taps completely.
Note that when you want MENU to go back to the home screen, you will need to temporarily remove this tap recognizer and let tvOS handle the tap on its own, as I could find no clean way to escape to the home screen in my code (short of exit(0)).
I have very similar problem because of this behaviour.
In my case I have custom focus management depending on MENU button click detected on pressesEnded and managed by preferredFocusedView. But when navigation controller is poping ViewController and user at this time make second click on MENU button, then UINavigationController class detect pressesEnded and then call preferredFocusedView on my destination ViewController where is my management but pressesEnded will be not called, because was "stolen" by UINavigationController.
Solution for this problem in my case is to create "dummy" class that will be used by UINavigationController like this:
class MenuPhysicalHackNavigationController: UINavigationController {
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
override func pressesEnded(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
override func pressesCancelled(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
override func pressesChanged(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
}

WinRT, Metro, Win8 remember list position w/o animation

I have a listview which i reload when i click on an item. I want to remember the scroll position so I use the following code:
private double scrollPosition;
public void SaveListPosition()
{
scrollPosition = scrollViewer.VerticalOffset;
}
public void ScrollToSavedPosition()
{
scrollViewer.ChangeView(0, scrollPosition, null, false);
}
Its working fine, but it shows the scrolling. After I change the ChangeView method's disableAnimation parameter to true it doesn't show scrolling as expected, but it completely messes up list positions and doesn't scroll to the element that I clicked.
So questions are:
1) is this a bug in winrt?
2) can I override the ChangeView's animation so it will instantly scroll exactly like supplying true for the disableAnimation parameter?
3) Any other solution?

Force portrait mode when dismissing a presented view controller

I have a presented view-controller that is supporting all interface orientations. However, the presenting view controller only should support portrait mode.
So far so good.
However, in iOS8, when I dismiss the view controller WHILE in landscape mode, landscape mode stays. And since I have shouldAutorotate set to NO it an never rotate back.
Question is, how can I force the presenting VC to return to portrait?
I currently have this workaround implemented:
- (BOOL)shouldAutorotate
{
if ([self interfaceOrientation] != UIInterfaceOrientationPortrait)
{
return YES;
}
else
{
return NO;
}
}
It allows to move the device into portrait and it will stay here, since after it's portrait autorotate is disable.
But it looks ugly until the user rotates his phone.
How to force it?
We had the exactly same problem.
You can rotate it programmatically by the code -
if ([UIApplication sharedApplication].statusBarOrientation != UIInterfaceOrientationPortrait) {
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
}
There are 2 possible options -
1) before you dismiss the presented viewController, rotate to portrait if
needed
2) after you dismiss, rotate to portrait in the "viewDidAppear" of the presenting viewController.
One issue with this, is that you can't pass a completion block, but you can use the next callback in iOS8:
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
if (self.needToDismissAfterRotatation)
self.needToDismissAfterRotatation = NO;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// dismiss
}];
}
}
By the way, in iOS8 apple did a huge change in the way the screen rotates, when the app rotates, the screen rotates, and all the elements in the UIWindow rotates as well, this is why when the presented viewController rotates to landscape, also the presenting viewController rotates, even if it only supports portrait...
We struggled with this issue for many days, finally we came up with a solution to put the presented viewController in a new UIWindow, and this way it keeps the presenting viewController in portrait all the time
example project for that:
"modalViewController" in UIWindow
This code will force the UI back to portrait, assuming that the view controller you're trying to force portrait was already the root view controller (if it wasn't already the root, this code will restore the root view controller but not any other view controllers that have been pushed onto it):
UIInterfaceOrientation orientation = [[UIApplication
sharedApplication] statusBarOrientation];
if (orientation != UIInterfaceOrientationPortrait) {
// HACK: setting the root view controller to nil and back again "resets"
// the navigation bar to the correct orientation
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIViewController *vc = window.rootViewController;
window.rootViewController = nil;
window.rootViewController = vc;
}
It's not very pretty as it makes an abrupt jump after the top level view controller has been dismissed, but it's better than having it left in landscape.

Swipe from one ViewController to another with Swift

I'm currently trying to connect different ViewControllers with each other using Swift, but I'm stuck and would appreciate some help in order to proceed.
What I have now is a main view and when you swipe to the right you would enter another view (like Snapchat works). I've done this using the tutorial found here.
Basically I have a ContainerViewController that store two subviews in its view hierarchy. So I can swipe back and forth between Subview 1 and Subview 2 (see illustration for an idea). Subview 2 has a button that 'modally' loads another ViewController. My question is, if it is possible, how could I make it so that when I swipe left in the "modal ViewController" I would swipe back to Subview 1?
Here is my code that controls the transition from Subview 2 (BViewController) and the modal ViewController (InfoVC):
class BViewController: UIViewController, UITabBarDelegate {
#IBOutlet var item: UIBarButtonItem!
var info: InfoVC = InfoVC(nibName: "InfoVC", bundle: nil )
func tabBar( tabBar: UITabBar!, didSelectItem item: UITabBarItem!){
if item.tag == 2{ // we are in new view controller
info.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
self.presentViewController(info, animated: true, completion: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
println("info hash \(info.hashValue)")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
And this is my ContainerViewController class where the ScrollView/swipe mechanism is made.
class ContainerViewController: UIViewController {
// Outlet used in storyboard
#IBOutlet var scrollView: UIScrollView?;
override func viewDidLoad() {
super.viewDidLoad();
// 1) Create the three views used in the swipe
var AVc: AViewController = AViewController(nibName: "AViewController", bundle: nil)
var BVc: BViewController = BViewController(nibName: "BViewController", bundle: nil)
// 2) Add in each view to the container view hierarchy
// Add them in opposite order since the view hieracrhy is a stack
self.addChildViewController(BVc);
self.scrollView!.addSubview(BVc.view);
BVc.didMoveToParentViewController(self);
self.addChildViewController(AVc);
self.scrollView!.addSubview(AVc.view);
AVc.didMoveToParentViewController(self);
// 3) Set up the frames of the view controllers to align
// with eachother inside the container view
var adminFrame :CGRect = AVc.view.frame;
adminFrame.origin.x = adminFrame.width;
BVc.view.frame = adminFrame;
var BFrame :CGRect = BVc.view.frame;
BFrame.origin.x = 2*BFrame.width;
// 4) Finally set the size of the scroll view that contains the frames
var scrollWidth: CGFloat = 2 * self.view.frame.width
var scrollHeight: CGFloat = self.view.frame.size.height
self.scrollView!.contentSize = CGSizeMake(scrollWidth, scrollHeight);
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
In summary:
What I can do already is to scroll from AViewController to BViewController (back and forth) and go from BViewController to InfoVC by pressing the tab bar item/button. What I then cannot figure out is to swipe backwards in InfoVC and reach AViewController with the same transition as when I swipe between AViewController and BViewController.
You need to add a UISwipeGestureRecognizer to your InfoVC. The easiest way, in Interface Builder, is to drag one from your Object Library onto your InfoVC. You'll see a new icon for it appear in the toolbar at the top of your IB scene. Select it, then in the Attributes Inspector, choose which direction you want the swipe to go--in your case, it's probably left to right.
Next, implement touchesBegan() and touchesEnded() methods in your InfoVC, and use the locations of the events to determine whether the user has swiped whatever distance you specify between the start and end points. If touchesEnded() is at least that distance away, then you can dismiss your InfoVC by removing it from its parent, BViewController.
Load the ViewController from your storyboard
Embed the BViewController in a UINavigation controller.
That will allow you to do all segues in a storyboard kind of fashion
// lets try to load them from the storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navigationController = storyboard.instantiateViewControllerWithIdentifier("navController") as UINavigationController
var BVc = navigationController
This will be a good first step. In your BVc prepare for Segue enabling and disabling the scrolling as it will interfere with the containerviews swiping. ReEnable it in your BVc's view did appear.
var parentVC = self.navigationController?.parentViewController as ContainerViewController
parentVC.scrollView?.scrollEnabled = false
You might have to do some additional work to re-receive swipe events in your modal, I am not sure.
I suggest using UIPageViewController, it was created for tasks like this and you can hide the dots bar by deleting these methods:
presentationCountForPageViewController
presentationIndexForPageViewController
For the button to work you need to add:
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [AnyObject], transitionCompleted completed: Bool) {
Here is a good tutorial:
https://www.youtube.com/watch?v=8bltsDG2ENQ