I'm trying to allow some views to rotate in my app (just two), I've done this before successfully, subclassing UINavigationcontroller and overriding the corresponding methods. The problem this time is that I'm using a third party project that creates the navigation controller from a .xib file (not programmatically). I changed the class in the .xib file in order to use my custom navigation controller and it does, but for some reason is ignoring override methods like shouldAutorotate and supportedInterfaceOrientations
Any ideas?
Thanks
I'm still looking for a more elegant way to handle this, but I'll share a little hack that has worked for me. If you find something better, please let me know.
An approach that has worked ok for me in apps with complex navigation trees, is to install the custom subclass for the very first navigation controller or very first view controller if you don't have a nav controller that is in your app. that is the one that is going to receive the shouldAutoRotate calls.
It sounds like you have done this already.
Now you need to insert your own logic in the shouldAutoRotate section. What I've done is to use a BOOL in the appDelegate as a place to store if a view controller should autorotate. Basically it works like this:
appDelete:
#property (nonatomic,assign) BOOL allowAutoRotationForThisViewController;
custom navigation controller at head of stack:
- (BOOL)shouldAutorotate {
AppDelegate *a = [[UIApplication sharedApplication] delegate];
return a.allowAutoRotationForThisViewController;
}
rotatable view controller:
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
AppDelegate *a = [[UIApplication sharedApplication] delegate];
a.allowAutoRotationForThisViewController = YES:
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
AppDelegate *a = [[UIApplication sharedApplication] delegate];
a.allowAutoRotationForThisViewController = NO:
}
I'm not sure why they changed this in ios6, it seems much more difficult, especially when you have complex navigation structure. I have an app with "slide" type controller at the top of the stack, a tabbar controller and stacks of navigation controllers. Trying to pick through all of those to get a web view on the bottom of the stack to rotate, or get them to respond to the top level controller is very complicated. So I've used this approach.
Now, one thing you have to consider - is that if the BOOL is turned to NO, then no rotation will occur - so one gotcha is if your rotatable view controller pops back to its parent while its rotated. Then the parent will be rotated and the rotatable view controller will have set the rotation value back to no. I solved this in my approach by preventing it to dismiss unless it was in portrait mode - basically I disabled the "back" button in landscape mode.
this works like this:
- view comes on screen - sets rotation to YES
- rotation occurs
- at this point, the view that is being rotated will get the following selector call:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
so, inside of that call, you would configure your local view and do anything you need to do to setup the view for the orientation change - like self.navigationController.hidesBackButton = YES;
hope that helps, and like I said, its a bit of a hack and I'm looking for something more elegant.
best of luck
Related
In a CompositeView, I implemented infinite scrolling like this
List.Foo extends Marionette.CompositeView
initialize: (collection) ->
#page = 1
$(window).on('scroll', #loadMore)
loadMore: =>
if _nearBottom
#page++
App.vent.trigger('list:foo:near_bottom', #page)
_nearBottom =>
$(window).scrollTop > $(document).height - $(window.height) - 200
# Then I have the controller to process the event "list:foo:near_bottom",
# to ask for adding one more page of data in collection.
The code basically works as expected. But I can't find it satisfactory as I think this ComposteView watches some DOM events outside of its scope, aka, the window level DOM events.
I thought to use a layout to watch such events and broadcast it, but my top level layout seems still not broad enough to cover window/document :)
My question is, what would be a better structure to watch these kinds of window/document level DOM event in Marionette? Thanks!
This question has not been answered for a long time, and I changed implementation in that project so I didn't touch it.
Nguyen's comment provided very nice point and reminds me to review this question.
I also have new understanding similar to Nguyen's point.
Something has to be global, we can't avoid it.
These things include but not limited to:
Route
Page scroll
Page load
Window resize
Global key stroke
...
Backbone has Routes to take care of routing events. The others things are not so important and so popular but they still need to be treated similar to routing.
A better approach would be, in my opinion: Watching the global DOM events at global level, send App event which don't care whoever may be interested in it.
If I re-do this feature, I will do something like this(pseudo code)
# App
App.on "initialize:after", ->
#startHistory()
#navigate('somePath', trigger: true) # Normal steps
App.module('WindowWatcher').start()
# WindowWatcher module
ExampleProject.module "WindowWatcher", (WindowWatcher, App, Backbone, Marionette, $, _) ->
class Watcher
constructor: ->
#watchPageScroll
watchPageScroll: ->
$(window).on('scroll', #_checkScroll)
_checkScroll: ->
if #_nearBottom:
App.vent.trigger(scroll:bottom)
_nearBottom:
$(window).scrollTop > $(document).height - $(window.height) - 200
WindowWatcher.on 'start' ->
new Watcher()
Then List.Foo controller will watch the App event scroll:bottom as he like, and supply next page.
There may be other parts interested in this event, for example in Footer view popping a button saying you are at bottom, or another notification saying if you want to see more you need to sign up, etc. They can also listen to the the App vent without need to manage window level DOM, thanks to the beauty of Marionette.
Important update
If you watch App vents directly inside controller but not at module level, make sure the controller will stop listen to this vent otherwise the listeners will increase in App.vents which is a memory leak.
# FooController
onClose: ->
App.vent.off 'scroll:bottom'
I am trying to optimize the code in my app. I have quite a few ViewControllers which all use a common "keypad".
I wanted to extract the keypad into a separate ViewController and then include it into the existing ViewControllers.
This way I could obliviate duplicate code (which was needed to deal with the reactions from the keypad) in the separate ViewControllers.
So in the KeyPadVC I have methods set up that look something like this.
-(IBAction)keyPadKeyPressed:(id)sender
{
[self.delegate interpretKeyPressed:[[sender titleLabel] text]];
}
In my "parent" ViewControllers I include the keypad by adding a subview to a plain UIView that I placed in Interface Builder (so that I have a visual placeholder) and hooked up to the variable keypadView.
-(void) viewDidLoad
{
[super viewDidLoad];
KeyPadViewController *kpVC = [[KeyPadViewController alloc] init];
[kpVC setDelegate: self];
[[self keypadView] addSubview:[kpVC view]];
}
This displays fine, but when I press a button on the KeyPadViewController I get a zombie object because the object was already released. I then tried to declare KeyPadViewController *kpVC in the #interface and tried a self instantiating method like:
-(KeyPadViewController *)kpVC
{
if (!kpVC) {
kpVC = [[KeyPadViewController alloc] init];
}
return kpVC;
}
I obviously modified the viewDidLoad method, but the result was always the same. The object gets released too soon. If I add NSLogs I can see that -(IBAction)keyPadKeyPressed from the KeyPadVC never gets called, because it KeyPadVC was already released.
What am I doing wrong? I am using ARC and iOS6 SDK.
Thanks
PS: this is pseudo-code to make things shorter - hope there are no typos - if so then that is not the issue. :)
KeyPadViewController *kpVC = [[KeyPadViewController alloc] init];
[kpVC setDelegate: self];
[[self keypadView] addSubview:[kpVC view]];
self.kpVC = kpVC;
That retains the view controller.
However, what you are doing is totally illegal because you are not using parent-child architecture. See my answer here: https://stackoverflow.com/a/15962125/341994
You can add a subview to your view, but you must not add a view controller's view as a subview to your view without going through the elaborate parent-child architecture. And you are not doing that.
I explain the parent-child architecture here:
http://www.apeth.com/iOSBook/ch19.html#_container_view_controllers
I need a good explanation how can I handle the UINavigationControllers and the UITabBarControllers on iOS6.1 with StoryBoards.
When I load my app (1st ViewController) I need if (FB login = success) it jumps with segues to the 2nd ViewController automatically. Here I think I can't use a UINavigationController like root, apple's HIG don't like it.
I need a UITabBarController that connects to 3 UICollectionViewControllers (one tab for each one). I have to put the UITabBarController like root? If yes, how can I handle the others Viewontrollers between them? Like this:
I need a custom BarButtonItem (like the "Delete All" that you can see on the image 2) on each CollectionViewController, I need to use a UINavigationController for each one?
Let's assume you are happy to use unwind segues (if not there are many ways to do without).
1 When I load my app (1st ViewController) I need if (FB login = success) it jumps with segues to the 2nd ViewController automatically. Here I think I can't use a UINavigationController like root, apple's HIG don't like it.
You 1st VC (lets call it the loginVC)..
- should NOT be contained in the Navigation Controller.
- should be set as the application's initialViewController
Your 2nd VC (lets call it your startVC)
- SHOULD be contained in the Navigation Controller
- in that Navigation Controller's Identity Inspector, assign a storyboardID: #"InitialNavController"
In your App Delegate, let's have a loggedIn BOOL property:
#property (nonatomic, assign) BOOL loggedIn;
Now, in your LogInViewController...
In viewDidAppear check to see if we are already logged in, if so, navigate immediately to your startVC:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if ([(AppDelegate*)[[UIApplication sharedApplication] delegate] loggedIn]) {
UINavigationController* navController =
[[self storyboard] instantiateViewControllerWithIdentifier:#"InitialNavController"];
[self presentViewController:navController
animated:NO
completion:nil];
}
}
It's important that this is placed in viewDidAppear, not (for example) in viewDidLoad - the segue won't work unless the initial view is properly initialised and onscreen.
Make an unwind Segue (and declare it in loginVC's #interface) … loginVC will be the destination VC if users log out.
- (IBAction)unwindOnLogout:(UIStoryboardSegue *)segue
{
[(AppDelegate*)[[UIApplication sharedApplication] delegate] setLoggedIn:NO];
}
(corrected - removed this line:
[[self presentedViewController] dismissViewControllerAnimated:YES
completion:nil];
we don't need to dismiss as the segue has already done that behind the scenes. This is redundant and logs an error message)
In other viewControllers, whereever appropriate you can make a 'logout' button. CTRL-drag from that button to the 'exit' symbol at the bottom of the ViewController in the storyboard, and you will be able to select this segue as the unwind segue.
2 I need a UITabBarController that connects to 3 UICollectionViewControllers (one tab for each one). I have to put the UITabBarController like root? If yes, how can I handle the others Viewontrollers between them? Like this:
I think you are trying to work out how the tabBarController relates to the NavigationController in the previous viewController (startVC). The answer is, it shouldn't - you really don't want to embed the Tab Bar VC in the previous Nav Controller as it will create weird situations for the Tab Bar's child viewControllers.
The navigation from startVC to the tabBarVC should be via a modal segue, NOT a push segue.
You can make another unwind Segue in startVC to facilitate return from your tabBarController's viewControllers:
- (IBAction)unwindToInitialNavFromModal:(UIStoryboardSegue *)segue {
}
(corrected - removed this line:
[[self presentedViewController] dismissViewControllerAnimated:YES completion:nil];
this method doesn't need any content to perform the dismissing)
3 I need a custom BarButtonItem (like the "Delete All" that you can see on the image 2) on each CollectionViewController, I need to use a UINavigationController for each one?
You won't get a Navigation bar in your tabBarVC by default.
You can provide one in two ways
- embed each child viewController in it's own Navigation Controller;
- manually drag a navigation bar to EACH child viewController's scene.
Either is fine, it really just depends whether you will want navigation to other ViewControllers.
You can then add a barButtonItem on the left or right to connect up to the initialVC's unwind segue (CTRL-drag to the 'exit' symbol).
SOLVED:
The only way to get around this problem (as far as I can tell) is to take your free rotating view controller outside of the hierarchy. One way to do this, is to temporarily assign your freely rotating controller to the app delegate rootcontroller. This way, when it rotates, the rest of your hierarchy will not be affected.
I have a controller which I push and the supportedInterfaceOrientations is called properly and the value is returned properly - I am forcing portrait orientation.
I have subclassed my navigator and tab controllers, so I am getting the relevant methods called fine. In fact everything works perfectly, except this one issue:
From a particular controller, which supports only portrait, I push another one, which supports all orientations.
While in this second controller, if I change the device to landscape, and then pop this controller while in landscape mode, the following occurs:
In my original controller, supportedInterfaceOrientations is called fine (shouldAutorotate is returning YES) and the same orientation value as before (portrait only) is returned. However, even though I pass porrtait only, the content of the controller is still landscape.
With break points, I have verified that supportedInterfaceOrientations is called every time. However, the returned value is not applied when a child controller landscape controller is popped.
I have tried adding
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait];
in viewWillAppear,
but:
1) It does not work and,
2) Will this not get my app rejected?
How can I solve this problem?
EDIT, here is some code, although my problem is very basic.
in controller 1, these are ALWAYS called:
-(BOOL)shouldAutorotate{
return NO;
///return YES; // does not matter
}
-(NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskPortrait;
}
then, at some point I push my second controller (it;s a video player):
[self presentModalViewController:playerController animated:YES];
in controller 2:
-(BOOL)shouldAutorotate{
return YES;
}
-(NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;
}
step to repeat:
1) launch app , it will show controller 1 in portrait mode as that's the mask returned. And it will not rotate.
2) push (or even present modaly) controller 2.
3) This one will rotate. Rotate to landscape mode.
4) pop controller 2.
5) controller 1 will be in landscape mode, even though, shouldAutorotate and supportedInterfaceOrientations are called and they return the values as shown in code for controller 1.
//add code in your controller 1
-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
I'm having troubles using the storyboard.
I properly connected a button (inside a custom uiviewcell) to a scene (ProfileVC) through push segue (whose identifier is pic2profile).
Besides in the table view controller (HomeVC) that manages the cells I implemented the method prepareForSegue:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:#"pic2profile"]){
ProfileVC *profile = (ProfileVC *)segue.destinationViewController;
NSLog(#"%#", [segue identifier]);
Tweet *tweet = [tweets objectAtIndex:0];
profile.user = tweet.user;
}
}
As explained in the documentation, once called segue.destinationViewController at the end of prepareForSegue should be displayed the view controller at the end of the segue (ProfileVC in this case).
What happen is that I get (no errors!) a black screen and the method viewDidLoad in ProfileVC is not called though the object "profile" is not null.
Inside the storyboard the ProfileVC scene is linked to its view controller (ProfileVC) and is also set up a Storyboard ID.
I already tried this but it didn't work.
Are you performing a segue to a view controller using the code you have, but have a navigation controller in between? If so you have to use a different way, specifying the navigation controller in between.
If you don't need the navigation controller you can delete it and connect the cell to the view controller and keep the code you have. Otherwise you have to change it.