Hello fellow programmers,
First, sorry for the long post. My question is rather simple, but I want to make sure you know what I'm doing and I really don't want to change the basic idea of my approach.
(the following is all done programmatically, no storyboards, no nibs, no navigationcontroller)
I have a RootViewController without an own view. All he does is instantiate other ViewControllers, manage their transitions and push (add) their views into the main window. To position these views correctly, I want to get bounds/frame for one of the RootViewCOntrollers SubControllers. This is how I create the RootViewController from the appDelegate (I started with a empty project)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor grayColor];
self.window.rootViewController = [[UCRootViewController alloc]init];
[self.window makeKeyAndVisible];
return YES;
}
After his initialization, the rootviewController creates a MapViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(#"RootviewController initialized");
self.mapviewController = [[UCMapviewController alloc] initWithDelegate:self];
self.view = self.mapviewController.view;
[self.mapviewController.view setOpaque:YES];
[self.view bringSubviewToFront:self.mapviewController.view];
[self presentModalViewController:self.mapviewController animated:YES];
self.currentlyActiveController = self.mapviewController;
}
The MapViewController creates a navigationBar and a MKMapView. Right now I set the frames hardcoded, because I'm not able to get the bounds/frame of the window in the viewDidLoad() of the MapViewController When I try to get any infos about bounds/frame, I get 0 returned.
- (void)viewDidLoad
{
NSLog(#"MapviewController initialized");
[super viewDidLoad];
self.isMapViewPushedAside = NO;
// Custom initizialation for navigationBar
[self setupNavigationBar];
// Custom initialization for mapview
[self setUpMapview];
[self trackUserLocation];
// Custom initialization for popupActionsButton
[self setUpPopupButtons];
// Custom tests
[self test];
[self focusLocationOnMap:self.locationManager.location.coordinate];
[self.locationManager stopUpdatingLocation];
}
I've implemented two delegate methods that return frame/bounds (same) for the window. The problem is, I must get those values at the start, not after everything has been initialized. when I call the delegate methods from a button after everything is up, they work as expected.
CGRect frame = [self.mapDelegate frameData];
NSLog(#"width by frame: %f", frame.size.width);
NSLog(#"height by frame: %f", frame.size.height);
CGRect bounds = [self.mapDelegate boundsData];
NSLog(#"width by bounds: %f", bounds.size.width);
NSLog(#"height by bounds: %f", bounds.size.height);
How do I obtain the frame/bounds at the start, that is, before calling my custom "setup" methods..?!
I have a RootViewController without an own view.
You can't have a UIViewController without a view. If you have the app will crash. When you initialize a UIViewController it automatically creates a UIView for you.
- (void)viewDidLoad
{
[super viewDidLoad];
..
self.mapviewController = [[UCMapviewController alloc] initWithDelegate:self];
self.view = self.mapviewController.view;
..
}
From what can I see here, you're actually setting the RootviewController's view to be the map view. This should be done by overriding the -(void)loadView method of your controller and there you need to set the view:
-(void)loadView
{
self.mapviewController = [[UCMapviewController alloc] initWithDelegate:self]; //if you're not using ARC this should be autoreleased;
self.view = self.mapviewController.view;
}
When viewDidLoad method is called there is no geometry set in any of the views of your controller. They are only initialized (implicitly or explicitly by -(void)loadView) and viewDidLoad is called just right after that. Geometry is setup at earliest in viewWillAppear: method and the consecutive viewDidAppear: method, so viewWillAppear: is the earliest point you can have your actual frame/bounds of your views and in viewWillAppear: method you should only execute some lightweight operations (like setting geometry, starting timers, subscribe observers, etc..).
You said you don't want to change your approach, but you need to design according to these rules.
Related
I'm creating a simple application with uitableview. I want to create everything in code. I used following code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
FBVCalendarViewController *calendarViewController = [[FBVCalendarViewController alloc] init];
self.window.rootViewController = calendarViewController;
[self.window makeKeyAndVisible];
return YES;
}
...
- (void)loadView
{
UITableView *calendarItems = [[UITableView alloc] init];
self.view = calendarItems;
}
it works, but application fills the entire phone screen intersecting with standard phone title bar.
What is the right way to adjust view height?
Since UITableView inherits from UIScrollView, you should take care of the changes appeared with IOS 7.
A solution to your problem is:
if ([self respondsToSelector:#selector(setNeedsStatusBarAppearanceUpdate)]) {
[self setNeedsStatusBarAppearanceUpdate];
self.automaticallyAdjustsScrollViewInsets = NO;
}
(this will keep the table view below the status bar).
Hope that helps. But you should probably have a look at changes introduced with IOS 7.
So I solved my problem with the following code in loadView:
- (void)loadView
{
UITableView *calendarItems = [[UITableView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
UIView *rootView = [[UIView alloc] init];
[rootView addSubview:calendarItems];
self.view = rootView;
}
I used empty UIView as a parent for tableView and changed constructor to explicitly specify UITableView frame. I think that better approach would be to use autolayout (currently it just does not work as expected when I rotate device) and position table view to the full screen or implement device rotation callback and update frame there.
I am trying to remove an iAd view from the view hierarchy. My implementation successfully removes the iAd banner from the view, but I continue to receive the following error:
Unhandled error (no delegate or delegate does not implement didFailToReceiveAdWithError:)
According to Apple documentation, removing the iAd view is reasonable in cases where the user navigates away from a screen that displays an iAd and you don't expect them to return to that screen for a while:
If the user navigates from a screen of content with a banner view to a screen that does
not have a banner view, and you expect them to be on that screen for a long period of
time, remove the banner view from the view hierarchy, set its delegate to nil and
release it before transitioning to the new screen of content. More generally, avoid
keeping a banner view around when it is invisible to the user.
So, I removed the banner view and set the delegate to nil, and this results in the banner disappearing from the view. However, I then start receiving the above mentioned error. Its not really clear how to do what Apple suggests. Here is what I have done. My use of iAd is in a class that is not a UIViewController (i.e. this is a cocos2d project). Hence, I utilize the RootViewController. In the header file of my class, I have:
RootViewController *viewController;
AppDelegate *app;
BannerViewController *bannerViewController;
In my class implementation file, I initialize the bannerViewController as follows:
app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
viewController = [(AppDelegate *)[[UIApplication sharedApplication] delegate] viewController];
bannerViewController = [[BannerViewController alloc] initWithContentViewController:viewController];
app.window.rootViewController = bannerViewController;
When I am ready to remove the iAd banner permanently, I attempt to remove the banner view from the view hierarchy and set its delegate to nil as follows:
if (bannerViewController) {
[viewController removeFromParentViewController];
bannerViewController._bannerView.delegate = nil;
[bannerViewController._bannerView removeFromSuperview];
[bannerViewController release];
bannerViewController = nil;
}
The bannerViewController is from Apples iAd Suite. The initialization and construction of the view hierarchy is as follows:
#interface BannerViewController () <ADBannerViewDelegate>
#end
#implementation BannerViewController {
ADBannerView *_bannerView;
UIViewController *_contentController; // RootViewController
}
- (instancetype)initWithContentViewController:(UIViewController *)contentController
{
self = [super init];
if (self != nil) {
// On iOS 6 ADBannerView introduces a new initializer, use it when available.
if ([ADBannerView instancesRespondToSelector:#selector(initWithAdType:)]) {
_bannerView = [[ADBannerView alloc] initWithAdType:ADAdTypeBanner];
}
else {
_bannerView = [[ADBannerView alloc] init];
}
_contentController = contentController;
_bannerView.delegate = self;
}
return self;
}
- (void)loadView
{
UIView *contentView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[contentView addSubview:_bannerView];
// Setup containment of the _contentController.
[self addChildViewController:_contentController];
[contentView addSubview:_contentController.view];
[_contentController didMoveToParentViewController:self];
self.view = contentView;
}
My RootViewController handles the observers for iAd BannerViewActionNotification's.
What I have noticed is that even though I release the bannerViewController, the dealloc method is not called. Like I said, the iAd banner does disappear from the view, but the error messages keep coming. This suggests that I have not properly disconnected from iAd and continue to receive ad notifications.
So, what am I doing wrong? How should I be removing the banner view from the view hierarchy per Apple's recommendation.
I am trying to set up a UIScrollView so that I can swipe between my 3 view controllers. This is my code in AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.;
UIScrollView *sv = [[UIScrollView alloc] init];
BarsViewController *bvc = [[BarsViewController alloc] init]; // Create BarsViewController
StopwatchViewController *svc = [[StopwatchViewController alloc] init]; // Create StopwatchViewController
TimerViewController *tvc = [[TimerViewController alloc] init]; // Create TimerViewController
[sv addSubview:bvc.view];
[sv addSubview:svc.view];
[sv addSubview:tvc.view];
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade]; // Hide status bar
self.window.rootViewController = sv;
[self.window makeKeyAndVisible];
return YES;
}
It gives an error on this line:
self.window.rootViewController = sv;
saying, "Incompatible pointer types assigning to 'UIViewController *' from UIScrollView *'".
However, there is no such thing as a UIScrollViewController, so I don't know what to do.
Basically, I just want the whole screen to be a scroll view which allows me to swipe between my 3 view controllers. How would I go about doing that?
UPD: June, 2015
Swift
The concept remains the same, which is described below in Objective-C section. There is a little change in syntax. To add childviewcontroller use following snippet:
let aViewController = storyboard.instantiateViewControllerWithIdentifier("A") as! AViewController;
addChildViewController(aViewController);
scrollView!.addSubview(aViewController.view)
aViewController.didMoveToParentViewController(self)
Check my Swift Github Sample Code
Objective-C
Create your own custom container view controller (I will call it combinedViewController), which will hold your three controllers in scroll view.
Inherit like you always do UIViewController, then use addChildViewController public API in your new combinedViewController -viewDidLoad: like this:
[self addChildViewController:aViewController];
[self.scrollView addSubview:aViewController.view];
[aViewController didMoveToParentViewController:self];
Here’s what the code does:
It calls the container’s addChildViewController: method to add the child.
It accesses the child’s view property to retrieve the view and adds it to its own view hierarchy. The container sets the child’s size and position before adding the view; containers always choose where the child’s content appears.
It explicitly calls the child’s didMoveToParentViewController: method to signal that the operation is complete.
Do this operation with each of your viewControllers. Afterwards, set your combinedViewController as a rootViewController.
if you need further explanation, feel free to ask.
Reference: Design custom container view controller
Here you are my Objective-C Github sample code
UPD: Thanks #Oliver Atkinson for clarifying that addChildViewController: method also calls the child’s willMoveToParentViewController: method automatically.
Results:
let obj1 = self.storyboard?.instantiateViewControllerWithIdentifier("DocumentsVC") as! DocumentsVC
let obj2 = self.storyboard?.instantiateViewControllerWithIdentifier("AppointmentsVC") as! AppointmentsVC
let obj3 = self.storyboard?.instantiateViewControllerWithIdentifier("DashboardVC") as! DashboardVC
self.containerScrollView.frame = obj2.view.frame
self.containerScrollView.addSubview(obj2.view)
obj2.willMoveToParentViewController(self)
self.addChildViewController(obj2)
self.containerScrollView.addSubview(obj1.view)
obj1.willMoveToParentViewController(self)
self.addChildViewController(obj1)
self.containerScrollView.addSubview(obj3.view)
obj3.willMoveToParentViewController(self)
self.addChildViewController(obj3)
self.containerScrollView.contentSize = CGSizeMake(3*UIScreen.mainScreen().bounds.width, 0)
obj1.view.frame.origin = CGPointMake(0, 0)
obj2.view.frame.origin = CGPointMake(UIScreen.mainScreen().bounds.width, 0)
obj3.view.frame.origin = CGPointMake(2*UIScreen.mainScreen().bounds.width, 0)
Swift 3.0
Based on Sachin answer - bit more generic - just add next element to views array
var views = [ViewController(), ViewController2(), ViewController3(), ViewController4()]
func setupScrollView() {
scrollView.frame = views.first!.view.frame
scrollView.contentSize = CGSize(width: CGFloat(views.count) * width, height: 0)
_ = views.map({ addViewToScrollView($0) })
_ = views.map({ $0.view.frame.origin = CGPoint(x: CGFloat(views.index(of: $0)!) * width, y: 0) })
}
func addViewToScrollView(_ viewController: UIViewController) {
scrollView.addSubview(viewController.view)
views.willMove(toParentViewController: self)
addChildViewController(viewController)
}
Try this git repo, Using this repo you can create a view navigation like Snapchat/Tinder Main Pages.
https://github.com/goktugyil/EZSwipeController
I have the following code in my main ViewController viewDidLoad function
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
nav = [[NavWithAutoRotateViewController alloc] initWithRootViewController:self];
[window addSubview:[nav view]];
[window makeKeyAndVisible];
[[self navigationController] setNavigationBarHidden:YES];
My ipad app is currently set to only work in landscape mode and I'm using this new window to show a quicklook document and allowing the nav bar to provide a back button and save options for the document. Main problem is that the new UIWindow orientation doesn't match my main applications UIWindow.
I have a custom UINavigationController above called NavWithAutoRotateController and here is the code for that controller.
-(id)init
{
if(self)
{
// _supportedInterfaceOrientatoin = UIInterfaceOrientationMaskLandscape;
// _orientation = UIInterfaceOrientationLandscapeLeft;
}
return self;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (BOOL)shouldAutorotate
{
return YES;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return YES;
}
-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscapeLeft;
}
// Tell the system which initial orientation we want to have
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationMaskLandscape;
}
I think I have a solution. The problem seems to be in the assignment of UIViewController to the rootViewController in your extra UIWindow.
If you just assume that you can use the same view controller that your primary UIWindow is using, and then add things as subviews of the new UIWindow, there are orientation issues.
To solve this, I did the following:
UIWindow *newWindow = [[UIWindow alloc] initWithFrame: [UIApplication sharedApplication].keyWindow.frame];
newWindow.windowLevel = UIWindowLevelStatusBar+1;
// You can use a view controller instantiated from a xib or storyboard here if you want.
// Just don't use the view controller already set as a UIWindow's rootViewController.
UIViewController *newViewController = [[UIViewController alloc] init];
newWindow.rootViewController = newViewController;
[newWindow makeKeyAndVisible];
// Add something to the new UIWindow. Can use the new UIViewController's view:
[newViewController.view addSubview: myContentView];
// Or could add it as subview of UIWindow: - either works.
[newWindow addSubview: myContentView];
Doing it this way seems to have solved all the weird issues around rotation.
Hope this helps.
I was having the same problem. Orientation changes were being handled properly using the viewWillTransition method. But my problem was that in some edge case conditions, namely if my device was sitting at an odd angle, the custom UIWindow would initialize in portrait orientation, even though the rootViewController was in landscape. And viewWillTransition isn't called because the device isn't rotated on initial load. I found a really simple solution to my problem that worked in any situation I tested.
First initialize your custom UIWindow without a frame. Then set your frame. And voila.. its orientation is what you'd expect.
Swift
let customWindow = UIWindow()
customWindow.frame = CGRect(x: x, y: y, width: w, height: h)
customWindow.rootViewController = self //<your_viewController>
customWindow.windowLevel = .statusBar // or whatever level you need
customWindow.makeKeyAndVisible()
ObjC
UIWindow customWindow = [[UIWindow alloc] init];
customWindow.frame = CGRectMake(x, y, w, h);
customWindow.rootViewController = self;
customWindow.windowLevel = UIWindowLevelStatusBar;
[customWindow makeKeyAndVisible];
Register for status bar frame change notification which is only called (afaik) when orientation changes and then define your new window's orientation..
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appWillChangeStatusBarFrameNotification:)
name:UIApplicationWillChangeStatusBarFrameNotification
object:nil];
My app has a root viewcontroller, which at the start of the app displays
login viewController view if the user is not logged in
main viewController view if the user is logged in
AppDelegate code:
- (BOOL) application: (UIApplication*) application
didFinishLaunchingWithOptions: (NSDictionary*) launchOptions
{
self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.rootViewController = [[RootViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
Here's the code used in RootViewController:
#implementation RootViewController
- (void) loadView
{
[super loadView];
// here mainViewController and loginNavigationController are initialized
}
...
- (UIView*) view
{
[super view]; // this invokes loadView
return self.isLoggedIn ? self.mainViewController.view :
self.loginNavigationController.view;
}
....
- (void) userDidLogin
{
[self.loginNavigationController presentViewController: self.mainViewController
animated: YES
completion: nil];
}
#end
If the user is not logged in and presses login button the main viewController is presented.
The problem is that after main viewController is presented, I'm not able to interact with any of the UI elements. For example, I have a tableView as a main viewController's subview and when I try to scroll it I get the following warning in debug panel:
<UITableView: 0x202a4000; frame = (0 0; 310 548); clipsToBounds = YES;
gestureRecognizers = <NSArray: 0x1fd9f570>; layer = <CALayer: 0x1fdccff0>;
contentOffset: {0, 0}>'s window
is not equal to <RootViewController: 0x1fd9f7d0>'s view's window!
Ok, so after looking at the updated code I see that you have a rootViewController and are dynamically giving the view you think should be presented. The thing is, the rootViewController is in charge of the root view while your other two view controllers manage their own views. You should not be passing a different view controller's view off.
So in the end it looks like you want to conditionally set your rootviewcontroller. So lets look at the app delegate. I think you should make your app delegate do something like this. Have it figure out at runtime which viewcontroller to present. Then make that the rootviewcontroller for the app.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIViewController * resolvedRootViewController = [self someMethodThatCorrectlyGivesRootViewController];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = resolvedRootViewController;
[self.window makeKeyAndVisible];
return YES;
}