In my app I've got a UINavigationController.
In some of the pages I got a MKMapView as the back view (allows changes to map view the overlay UIView UI, so I can't make it as a image).
In some combinations, It's possible to have 3 or so MKMapView alloc'd.
The problem is that each MKMapView takes nearly 60MB, so the memory jumps up to 180+ MB only from the maps. not to mention if the user opens more ViewControllers with map, he'll get a OutOfMemoryException.
All the maps have the same content, I tried moving a map from ViewController to another, but according to another post in StackOverflow, moving UIView from ViewController to another is against MVC, not to mention it removes it from the caller ViewController.
Basically since all the maps contains the same content, and I want them the contain all the annotations it'd be best to just move the map but I had hard time with that.
I've got a solution out of the box which is to remove the MKMapView from its superView and on viewWillAppear realloc it, but this doesn't seem like the best idea.
I don't see any problem with passing a pointer to a single map view when you move from controller to controller. Unless you're viewing more than one map at a time, I don't see why there should be more that one instance. Remove the view in viewWillDisappear, and pass a pointer to the map in prepareForSegue:
- (void)viewDidLoad {
[super viewDidLoad];
if (! self.mapView) {
self.mapView = [[MKMapView alloc] initWithFrame:self.view.frame];
}
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.view addSubview:self.mapView];
[self.view sendSubviewToBack:self.mapView];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.mapView removeFromSuperview];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
SecondViewController *secVC = segue.destinationViewController;
secVC.mapView = self.mapView;
}
Subsequent controllers could have the same code in viewWillAppear, viewWillDisappear, and prepareForSegue (except for what controller is the destinationViewController). The map only needs to be instantiated once in the viewDidLoad method of the first controller. An even simpler implementation would be to have all the subsequent view controllers inherit from the first controller. If you do it that way, you won't need to put anything in viewWillAppear or viewWillDisappear. You only need to override prepareForSegue.
Related
I'm developing an application which will work based on maps. So once user opens MapViewController then I will load some data every 5 seconds.
I'm using navigation controller(Push view controller).
So every time when user goes to MapViewController viewdidload method calling. I don't want like that.
That's why I'm trying to avoid viewdidload method like tabbarcontroller.
Is there any way to achieve this?
viewDidLoad is getting called because your MapViewController is getting deallocated when you pop it off of the top of your navigation controller. When you recreate the view controller, it's getting allocated all over again, and the view loads again. If you keep a reference to MapViewController in the class containing your navigation controller, then ARC will not deallocate the object, and you can use this reference to push it back onto the stack so viewDidLoad will not get called again.
Edit: Adding code for reference:
MapViewContoller *mapViewController; // declared globally so there's a strong reference.
- (void) openMapViewController {
if (!mapViewController) {
mapViewController = [self.storyboard instantiateViewControllerWithIdentifier: MapViewControllerID];
}
[self.navigationController pushViewController: mapViewController, animated: YES];
}
Try this
-(void)clickForPush{
// declarre viewcontroller e.g 'saveRecipeVC' instance globally in interface
if (!saveRecipeVC) {
saveRecipeVC = [self.storyboard instantiateViewControllerWithIdentifier:SaveRecipeVCID];
}
[self.navigationController pushViewController:saveRecipeVC animated:YES];
}
viewDidLoad is intended to use when,not possible or efficient to configure 100% of an interface in a XIB. Sometimes, a particular property you wish to set on a view isn't available in a XIB. Sometimes, you use auto layout, and you realize that the editor for that is actually worse than writing auto layout code. Sometimes, you need to modify an image before you set it as the background of a button.
If you dont want to do these things make your viewDidLoad empty. Than avoiding. Or
Add code conditionaly into your viewDidLoad.
(void)viewDidLoad {
[super viewDidLoad];
if(condition) {
// put your code here
}
}
You can see on the gif below that on the first scroll of UITableView cell's content moves a tiny bit. You can barely see it, margins become 1 pixel wider.I've never encountered this before. It seems like there's some layout issue before the first scroll and it resolves itself after the fact. There's no warning in XCode, these custom cells are pretty straightforward, with no layout code overrides.
I don't know where to start, how do I catch this glitch?
UPDATE. I've implemented an obvious workaround for now:
- (void)scrollTableToFixGlitch {
[self.tableView setContentOffset:CGPointMake(0, 1)];
[self.tableView setContentOffset:CGPointMake(0, -1)];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self scrollTableToFixGlitch];
}
Still looking into the problem. I've tried generic UITableViewCells, nothing changed. Seems like it's the problem with View Controller's root view or tableview layout, and not with the table cells.
UPDATE 2.
I ruled out all the animations out of the question, problem lies somewhere in a different region. The glitch is easy to recreate on a much simplified project. My Tab Bar controller is based on MHCustomTabBarController with custom segues and some other additions. Here's what you do to recreate a glitch. Setup a project where your initial VC is embedded in Navigation Controller. The next controller either MHCustomTabBarController or a subclass is pushed to the navigation stack. First visible VC in tab bar is generic Table View Controller. That's it. Glitch appears only if tab bar controller is pushed in navigation stack.
Here's some code that I think matters from tab bar controller:
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.childViewControllers.count < 1) {
[self performSegueWithIdentifier:#"viewController1" sender:[self.buttons objectAtIndex:0]];
}
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if (![segue isKindOfClass:[MHTabBarSegue class]]) {
[super prepareForSegue:segue sender:sender];
return;
}
self.oldViewController = self.destinationViewController;
//if view controller isn't already contained in the viewControllers-Dictionary
if (![self.viewControllersByIdentifier objectForKey:segue.identifier]) {
[self.viewControllersByIdentifier setObject:segue.destinationViewController forKey:segue.identifier];
}
[self.buttons setValue:#NO forKeyPath:#"selected"];
[sender setSelected:YES];
self.selectedIndex = [self.buttons indexOfObject:sender];
self.destinationIdentifier = segue.identifier;
self.destinationViewController = [self.viewControllersByIdentifier objectForKey:self.destinationIdentifier];
[[NSNotificationCenter defaultCenter] postNotificationName:MHCustomTabBarControllerViewControllerChangedNotification object:nil];
}
And a custom segue code:
#implementation MHTabBarSegue
- (void) perform {
MHCustomTabBarController *tabBarViewController = (MHCustomTabBarController *)self.sourceViewController;
UIViewController *destinationViewController = (UIViewController *) tabBarViewController.destinationViewController;
//remove old viewController
if (tabBarViewController.oldViewController) {
[tabBarViewController.oldViewController willMoveToParentViewController:nil];
[tabBarViewController.oldViewController.view removeFromSuperview];
[tabBarViewController.oldViewController removeFromParentViewController];
}
destinationViewController.view.frame = tabBarViewController.container.bounds;
[tabBarViewController addChildViewController:destinationViewController];
[tabBarViewController.container addSubview:destinationViewController.view];
[destinationViewController didMoveToParentViewController:tabBarViewController];
}
#end
UPDATE 3
During my research I've found that - viewWillAppear is not called the first time when child controller appears. But it's called in all subsequent times.
Maybe the scrollviews contentSize is wider than your scrollView's frame(width specifically in this case) causing scrolling for both directions.
You can either try to decrease the contentSize width to the scrollView's width and
self.scrollView.alwaysBounceHorizontal = NO;
If this doesn't work, the solution would be to disable horizontal scrolling programatically by the help of the UIScrollView delegate
self.scrollView.delegate = self;
[self.scrollView setShowsHorizontalScrollIndicator:NO];
//for the below UIScrollView delegate function to work do the necessary step in the bottom of my answer.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x > 0)
scrollView.contentOffset = CGPointMake(0, scrollView.contentOffset.y);
}
And in your .h file you should change the interface line to below by adding UIScrollViewDelegate
#interface ViewController : UIViewController <UIScrollViewDelegate>
You most probably know this delegate part very well but for others it might be needed:D
Original answer
Ah, I've finally found the origin of this behaviour. I was almost sure this is happening due to some of the preparation methods are not called properly. As I stated in the update 3 I've found that -viewWillAppear method is not called in TableViewController when my TabBarController is pushed to the navigation stacked. I've found a ton of coverage of this matter on SO, it's a very well known issue apparently.
I've added a simple fix just to check if I'm right in my Custom Segue:
//added this line
[destinationViewController viewWillAppear:YES];
[tabBarViewController.container addSubview:destinationViewController.view];
And it works like a charm, no flickering! Now I have to figure out a more suitable place for this call, since explicit calls to methods like this can break a lot of stuff.
Probably the best place is in -navigationController:willShowViewController:animated: method of UINavigationControllerDelegate.
Anyway, problem solved. Hope it will help someone with the same issue.
UPDATE Actually, I was not completely correct on that. -viewWillAppear is called on my tab bar controller when it's pushed to navigation stack. It's not being translated to TableViewController. So there's no need to access NavigationControllerDelegate. Simple fix to a custom segue is enough.
I am attempting to accomplish something and i was looking for a few tips. I started working on a project that consisted of one xib file. Later on i upgraded to xcode 5 and pretty soon for the rest of my interface files i began using my storyboard however i kept my old xib (mainly cause of how i pass in methods to it) and simply called it like :
mapViewController *mapView=[[mapViewController alloc]initWithNibName:#"mapViewController" bundle:nil location:0 option:#"poe"];
Recently I have added new functionality and upon clicking a callout I would like to prepare a segue for a new view to be displayed.
Firstly in the xib interface. I was wondering if I could from my mapView (not in storyboard) prepare and call a view/segue(in story board) .
Or if I can simply drop or copy and paste my mapView into my storyboard and then draw my segue connectors to my new view. As I dont think it is necessarily for my to add the mapView to my "story line" however I still want to call is using the method posted above.
However if none of these are feasible please suggest another method.
Thanks
UPDATE
I have moved my xib to my storyboard interface however i am facing a little problem
When i called my map from the xib it would zoom in to a specific location supplied in the init method. I am passing in the same parameter but i am having a little trouble recreating my old results in my storyboard
if ( ([receivedRainObject isEqualToString:#"Natural"])) {
// mapViewController *mapView=[[mapViewController //alloc]initWithNibName:#"mapViewController" bundle:nil location:0 option:#"na"];
// self.navigationItem setHidesBackButton:NO animated:NO];
// [self.navigationController setNavigationBarHidden:NO animated:YES];
// [self.navigationController pushViewController:mapView animated:NO];
[self performSegueWithIdentifier:#"countryMap" sender:self];
}
Then in preparing the srgue
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"countryMap"]) {
mapViewController *transferViewController=segue.destinationViewController;
transferViewController.location=0;
transferViewController.option=#"na";
}
}
Then in view did load of the map
location does evaulate to 0 and the breakpoint shows me that all this code is being stepped thru but the region of the coordinates i supplied isnt what is being shown.( the the span or region)
(however when i call my old xib it works fine)
- (void)viewDidLoad
{
[super viewDidLoad];
{
// Do any additional setup after loading the view from its nib.
if (location==0)
{
CLLocationCoordinate2D location1;
location1.latitude=(double)15.435786;
location1.longitude=(double)-61.318447;
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta=0.50;
span.longitudeDelta=0.40;
region.span=span;
region.center= location1;
[mapView setRegion:region animated:TRUE];
[mapView regionThatFits:region];
}
}
INIT of my mapview
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil location:(int)num option:(NSString *)row
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
self.location=num;
self.option=row;
}
return self;
}
In your mapView, you should be able to do something like this in the callout method:
UIStoryboard *sb = [UIStoryboard storyboardWithName:#"YourStoryBoardName" bundle:[NSBundle mainBundle]];
YourViewControllerFromStoryBoard *viewController = [sb instantiateViewControllerWithIdentifier:#"YourViewControllerIdentifierInYourStoryBoard"];
Then in your mapView you can override prepareForSegue to do any prep work and then present your viewController.
You can't call a segue between a xib defined controller and a storyboard defined one (or between controllers in two different storyboards either). You can instantiate the controller in the storyboard using the UIStoryboard method, instantiateViewControllerWithIdentifier:, and then push or present it in code. It's not clear from your question why you don't want to move the xib based controller into your "story line". You can create a new controller there, and copy and paste the view from your xib file if you want to go that route.
I've a bunch of turn based games in my app, and I use the same animations to declare the starting player.
At the very end of viewDidLoad, I placed the code for declaration. It takes the screenshot of current view then blurs it a little, and labels appear to show the name of the starting player. The issue is sometimes it happens to fast that I got the screenshot of previous view and labels appear on the blurred screenshot of previous view.
My viewDidLoad looks like this:
-(void) viewDidLoad
{
[super viewDidLoad];
[self initializeThings];
[self layoutUI]; //In some of the games this part requires heavy processing,
//ie laying out a 2D array of buttons (20x20=400 of them)
[self showStartingPlayer];
}
I use the default transition style cover vertical in all VCs. I tried calling [self showStartingPlayer]; deferred by using performSelector with delay but different devices require different delay values so it is not a robust solution. Is there any other method I can use in viewcontroller lifecycle instead of viewDidLoad or any practical way of doing such a thing?
if you are using presentViewController: animated: completion:, i would take advantage of the completion block to notify the view controller that the transition is complete.
for example, you could add a public method called -(void)wasJustPresented to your view controller which calls the necessary UI layout.
Then, call this in your completion block. Ex:
[self presentViewController:newVC
animated:YES
completion:^(void){
[newVC wasJustPresented];
}];
This will ensure your view controller is notified right after it is done being presented.
viewDidLoad is called when the view of the view controller has been loaded, but it doesn't mean that it's actually visible on the screen.
You may use - (void) viewDidAppear: to do that.
You should try to call your method inside the viewDidAppear which is called as the view transition has finished.
-(void)viewDidAppear:animated
{
[super viewDidAppear:animated];
//put your call here
}
I am trying to get a popup effect and want to design the popup view in another view controller so i can use the xib to do it.
When i used the presentViewController or pushViewController and set the background to transparent, i end up seeing the Window's background color.
I tried this code to add subview to the navigation controller's view so that i can have the Info view cover the entire screen with a transparent background. I also have tab bar to cover up as well.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
My problem is inside my InfoVC when i try to dismiss it, the app will crash with some EXC_BAD_ACCESS message:
[self.view removeFromSuperview];
EDIT:
I found a way to stop it crashing but setting the InfoVC as a property in the MainVC. I think the reason for crash is when i call "self.view" in the action inside the InfoVC, it doesn't know that self is the InfoVC inside MainVC.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
No no no no. Never never do that.
There is an elaborate dance that you must traverse in order to put a view controller's view inside another view controller's view (or remove it afterwards) if it doesn't come with built-in facilities for doing this (the way a UISplitViewController does, or the way a navigation controller manages the views of the view controllers that are pushed and popped within it).
Read up on customer container controllers. One of the examples from my book is here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch19p556containerController/p476containerController/ViewController.m
Shouldn't you be using the following to remove the view from its superview?
[vc.view removeFromSuperview];
You can never have a UIView remove it's subviews, the subviews themselves must remove themselves from it's superview. You can easily loop through subviews and have them removed like so
for (UIView *view in vc.view.subviews) {
[view removeFromSuperview];
}
Docs for reference:
http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
After a "modally" presented view controller has appeared the views under the now presented view controller will be removed; this saves memory, and eases rendering. In your case, though, you also end up seeing the window behind the "modally" presented view.
The natural, and seemingly logical, next step is to simply take one view controller's view and cram it into another. However, as you have discovered, this is problematic. With the newly inserted view safely retained by the view hierarchy it is safe, but the new view controller is not so lucky, it is quickly deallocated. So when this new view tries to contact its controller you will get an EXC_BAD_ACCESS and crash. One workaround, again as you have found, is to simply have the original view controller keep a strong reference to the new view controller. And this can work... badly. There's still a good chance you will get an UIViewControllerHierarchyInconsistencyException.
Of course if you simply want to add a small view you create in IB you don't need to use a view controller as the "File's Owner" and there are many examples of creating an instance of a view from a xib file.
The more interesting question here is, "How would/does apple do it?" Apple consistently says that a view controller is the correct controller for an encapsulated unit of work. For example, their TWTweetComposeViewController, you present it, and it seems to float. How?
The first way of accomplishing this that comes to my mind is to have a clear background that isn't clear. That is, create an image of the screen before the presented view controller appears and set that as the background before the presenting view is removed. So for example(Explanation to follow):
QuickSheetViewController.xib
QuickSheetViewController.h
#import <UIKit/UIKit.h>
#interface QuickSheetViewController : UIViewController
- (IBAction)dismissButtonPressed:(id)sender;
#end
QuickSheetViewController.m
#import "QuickSheetViewController.h"
#import <QuartzCore/QuartzCore.h>
#implementation QuickSheetViewController {
UIImage *_backgroundImage;
}
-(void)renderAndSaveBackgroundImageFromVC:(UIViewController *)vc{
UIGraphicsBeginImageContext(vc.view.bounds.size);
[vc.view.layer renderInContext:UIGraphicsGetCurrentContext()];
_backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// save an image of the current view, and set our background to clear so we can see the slide-in.
[self renderAndSaveBackgroundImageFromVC:self.presentingViewController];
self.view.backgroundColor = [UIColor clearColor];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Time to use our saved background image.
self.view.backgroundColor = [UIColor colorWithPatternImage:_backgroundImage];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// Set our background to clear so we can see the slide-out.
self.view.backgroundColor = [UIColor clearColor];
}
- (IBAction)dismissButtonPressed:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
The majority of this example hinges upon the renderAndSaveBackgroundImageFromVC: method. In which, we create a graphics context render the view we are about to cover into it, and then create a UIImage to later (in viewDidAppear) use as a background.
Now simply use it like:
QuickSheetViewController *newVC = [[QuickSheetViewController alloc] initWithNibName:nil bundle:nil];
[self presentViewController:newVC animated:YES completion:nil];
You will see through the background just long enough for the animation to happen, then we use our saved image to hide the removal of the presenting view.