iOS: Autolayout with child view controllers - ios

I've been using autolayout for a couple of weeks now.
Currently, I'm using a 3rd party library called FLKAutoLayout which makes the process easier.
I'm at the point where I can construct views the way I want, usually without problem.
However, over the past 4 days at work I've been struggling with autolayout once viewcontrollers are involved.
I'm fine with all sorts of UIViews... but for some reason, every viewcontroller.view is a total demon.
I've had nothing but problems getting a viewcontroller.view to size the way I want and an ever deeper problem is that UIViews of child viewcontrollers do not receive events properly when using autolayout.
child viewcontrollers work just fine when designating frames manually, but everything breaks down with autolayout.
I don't understand what's so different about a viewcontroller's UIView that makes it different than all the others... My mind is melting in frustration. Is ios messing with my viewcontroller views behind the scenes or something?
In the image, the red area belongs to a child view controller. This area should not be going past the bottom most subview (the card that says three). This should be easy and I can get it to work just fine with a bunch of normal UIViews but because this is a viewcontroller, everything breaks...
Can anyone shed light on what it is I don't know. Any leads on potential issues is much appreciated.
Thanks for reading.
Update: The problem may be related to ambiguous constraints
UIView *box = [[UIView alloc]init];
[box addSubview:imageView];
[box addSubview:nameLabel];
imageView constrainWidth:#"32" height:#"32"];
[imageView alignTop:#">=0" leading:#"0" bottom:#"<=0" trailing:#"<=0" toView:box];
[imageView alignCenterYWithView:box predicate:#"0"];
[nameLabel constrainLeadingSpaceToView:imageView predicate:#"5"];
[nameLabel alignTop:#">=0" leading:#">=0" bottom:#"<=0" trailing:#"<=0" toView:box];
[nameLabel alignCenterYWithView:box predicate:#"0"];
[self addSubview:box];
[box alignTop:#"5" leading:#"5" bottom:#"-5" trailing:#"-5" toView:self];
The example above is an ambiguous layout but I can't figure out what's wrong with it...

This should probably be a comment, but comments suck for code. ;-)
Did you check for ambiguous constraints? To me this view looks like it could can be caused by ambiguous constraints.
Add this code to your app delegate:
// before #implementation AppDelegate
#interface UIWindow (AutoLayoutDebug)
+ (UIWindow *)keyWindow;
- (NSString *)_autolayoutTrace;
#end
// inside of #implementation
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
NSString *autolayoutTrace = [[UIWindow keyWindow] _autolayoutTrace];
if ([autolayoutTrace rangeOfString:#"AMBIGUOUS"].location != NSNotFound) {
NSLog(#"%#", autolayoutTrace);
}
else {
NSLog(#"No Ambiguous autolayout found");
}
}
and now shake your simulator. You'll find the shake gesture in the hardware menu.
If it doesn't show "No Ambiguous autolayout found" check for the ambiguous ui element(s) in the trace that was printed. They are marked with "AMBIGUOUS".
Then start to add constraints so there is no more ambiguity.
You can call exerciseAmbiguityInLayout on an UI-Element that has an ambiguous layout to get a hint which constraints are missing.
And make sure to remove the debugging code in your shipping product. You might put those two parts inside of #if DEBUG and #endif

At a certain point in your code you need to add the child view controller to the view controller hierarchy: [topViewController addChildViewController:childViewController];. And don't forget to add -didMoveToParentViewController: afterwards.
This will ensure that your rotation and touch events are forwarded the way you are expecting.
As for your autolayout problems: Erica Sadun has written some very useful autolayout debugging tools. I use it mostly to look at the viewLayoutDescription that she has written in a category on UIView, which prints a very readable lists of the constraints, tells you about ambiguity etc. Give it a try, it really helped me figure out how my constraints were messed up.
Second: always make sure that all of your views have translateAutoresizingMasksIntoConstraints set to NO.

Related

Updating Constraints programmatically iOS

I need to update a constraint programmatically in my project to adjust a view's height accordingly (based on different subviews). I've made outlet of the constraint in my controller but facing an issue.
when I try to update this for the first time (in viewDidLoad or viewWillAppear method), it's not updated. and if i update it afterwards (because of some rotation etc), then it is done correctly. Kindly tell me why this is happening and what is the right way/place to do this? (As i feel that the constraint is updated somewhere again after my updation in viewWillAppear/DidLoad).
I tried view.layoutIfNeeded as well but it didn't help. I guess it has something to do with viewDidLoad and other viewController delegate methods
P.S. I'm also using size classes in my project but I think it has nothing to do with that as it's working in some cases.
Updating constraints may not work in viewWillAppear.
It will, however, work in viewDidAppear.
There are other places you may overwrite, such as: (using static BOOL shouldRefreshView = NO; for the first time)
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if(shouldRefreshView) {
shouldRefreshView = NO;
[self.view layoutIfNeeded];
}
}

Where to add UI objects when programmatically creating UI (instead of .xib/storyboard)

In the UIViewControllers where I don't use .xib files, I've been creating my UI elements in the viewDidLoad methods. E.g.,
- (void)viewDidLoad {
[super viewDidLoad];
// Setup table
self.tableView=[[UITableView alloc] initWithFrame:self.view.frame];
[self.tableView setDataSource:self];
[self.tableView setDelegate:self];
[self.view addSubview:self.tableView];
// Setup custom cells
UINib *lessonNib=[UINib nibWithNibName:#"CustomCell" bundle:nil];
[[self tableView] registerNib:lessonNib forCellReuseIdentifier:#"CustomCellID"];
}
For the most part, this is working fine. But, in the process of investigating a bug associated with dynamic cell heights, I'm curious: is this the appropriate spot to add my UI elements?
Thanks for reading.
I would say it is the right place, yes. Since this method is only called once for sure and is called before anything of your view is shown on screen. And therefore you don´t risk creating them twice or even more often or too late.
But your layout should be done at a different place - in viewWillLayoutSubviews:
When a view's bounds change, the view adjusts the position of its subviews. Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.
You could also overwrite the corresponding viewDidLayoutSubviews
Regarding your comment: yes, layout information is not yet present in viewDidLoad, self.view.frame for example is not guaranteed to be the actual frame that your view will be displayed in later on. Further more a frame change by some part of your code would cause your UI to not respond if you set their size and position only on load.
Note: Setting up your subviews via code is far more tedious than just designing them in a storyboard - I would heavily recommend that if you don´t have serious concerns against using them.

iOS 7.1 removeFromSuperview crash

My app didn't have any crash until iOS 7.1 came out. Now on any removeFromSuperview method, crash. For example: I got view controllers, and when I want to remove a view controller, I remove all of its subviews, and then remove from the stack (stack: I'm storing view controllers in this, for load new contents, and load previous contents):
for (UIView *subView in [contentVc subviews])
[subView removeFromSuperview];
And I got
-[CALayer retain]: message sent to deallocated instance
message
[actual removeFromParentViewController];
is a good way to remove it? And will it release the whole view controller and its subviews? Because instead of removeFromSuperview, my app doesn't crash. I don't understand what have been changed in iOS 7.1.
And how can I remove all subviews in a viewController without removeFromSuperview, and without remove my ViewController (if I just want to add new subviews, and remove the currently content)?
UPDATE:
sometimes crash for:
[myactualviewcontroller.view removeFromSuperview];
-[CALayer retain]: message sent to deallocated instance
Why???
and sometimes if I try to remove the main subview from the view controller view, its got the same crash:
[mainView removeFromSuperview] ( mainView is a single UIView, added to the vc.view )
UPDATE2: (well detailed)
so, I've got a container view. I'm adding a UIViewController.view to this container. And I'm adding a view as a subview to UIViewController.view. This view is not a local uiview, I mean, its declared as implementation{ UIView* mainView } .When my UIViewController will be deallocate, in its - (void) dealloc { [mainView removeFromSuperview]; [mainView release] [super dealloc];}
At the mainView removeFromSuperview my app crash.
It's usually not a good idea to modify an array while you're fast enumerating it. You appear to be using fast enumeration on a a view's array of subviews, and to be modifying that array at the same time (by removing subviews as you go). You could try something like this:
NSArray *subviewsCopy = [[contentVc subviews] copy];
for (UIView *subview in subviewsCopy) {
[subview removeFromSuperview];
}
However, as some others have mentioned, it's a little odd that you need to go to the trouble of removing these subviews manually. Under normal circumstances a view controller's view (and the view hierarchy under it) will be cleaned up automatically when the view controller itself is deallocated.
There are also some good tools available that can help you track down the source of the issue. In particular, you should profile your app (in Xcode, under the Product menu) and choose the Zombies tool when Instruments prompts you. With Zombies you can see the retain/release history of an object that was messaged after it was deallocated.
If you're attempting this manual cleanup of the view hierarchy because you suspect that your views will be leaked otherwise, I suggest that you also try the Leaks tool in Instruments and verify that when this code is disabled the relevant views are actually leaked.
Your program is crashing because you are releasing something more than once. That part is obvious.
The first step in finding it is to enable zombie detection in the debugger. (Project->Schemes->Edit Scheme->Diagnostics->Enable Zombie Objects). The goal here is to make your program crash sooner. This will drop you into the debugger as soon as you try to access a deallocated instance. Sometimes this will point you in the right direction, sometimes not, but it's always better to detect it as close to where the problem is as possible.
The next step is to use the Zombies instrument. This tool will give you more information than the previous step, but it's more complex to use (which is why I made it step 2 instead of step 1). The Zombies tool will keep track of all your allocations and releases, and detect when you try to access a zombie object.
The last resort is to start commenting out code. First comment out everything your program does between the time you create the view controller (the one that crashes) and when you release it. Then run the program and do whatever you need to do to make it display the bad view controller. It won't do anything, obviously, because it's just an empty view controller now, but it should not crash). Then start uncommenting blocks of code, a little bit at a time, and keep running it in between each iteration. This is a repetitive process, and can be tedious if your view controller code is large and complex. But the idea is to keep adding your code back in little by little until you add something back and it crashes - then you know you've found the piece of code that's causing the problem. You have to be creative here and choose carefully how you put your code back in - if your program has a nice modular design, you should be able to do this without much trouble. Spaghetti code will be difficult to do this with, but it might give you a good opportunity to restructure your code while you're at it. By going through this process, you'll narrow down the problem and eventually find the bug by process of elimination.
UPDATED
try to do this:
NSArray *subviews = [NSArray arrayWithArray:[contentVc subviews]];
for (UIView *subView in subviews)
[subView removeFromSuperview];
I think that you got the crash beacuse you're trying to fast enumerate an array that has variable length (in fact when you remove a subview, it is removed also from subview array).
If you want to remove the viewcontroller, just call:
[contentVc.view removeFromSuperView];
[contentVc removeFromParentViewController];
sometimes crash for:
[myactualviewcontroller.view removeFromSuperview];
You shouldn't add or remove a controllers' view from a view hierarchy manually but rather rely in UIWindow's rootViewController, push your controller to a UINavigationController, etc., to get the system to add the view to private underlying superviews. Unless your creating a Custom Container View Controller, which I guess you aren't.
If you just want to handle views manually don't use view controllers as they won't get retained by the system and they won't get any rotation messages, etc. So using a view controller is pointless in that case anyway.
As for subview memory handling, subviews are retained by their superview, so as long as you don't keep a strong reference, you don't need to release subviews, just remove a common superview.
Again, if you properly use view controllers just releasing the controller will get rid of all views.
Finally, you should start using ARC.
1.According to the Apple's documentation, calling removeFromSuperview will remove that view from superview and release it automatically.
So if you are using removeFromSuperview, then you should not call the [removedView release], which will crash your App.
Refer this screenshot from Apple. 
In your dealloc implementation, you are having like so
- (void) dealloc {
// Removed from Parent view and released.
[mainView removeFromSuperview];
// no mainView exists in memory , so it crashed the App.
[mainView release];// Comment this line to avoid the crash
[super dealloc];
}
2.You should not mute the container that are being enumerated.
You are having like this,
for (UIView *subView in [contentVc subviews])
[subView removeFromSuperview];
Instead you can implement the same effect by having this one line from Apple.
[[contentVc subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
Please be sure that all of possible delegates removed before views deletion (i.e. someScrollViewDelegate = nil; before [someScrollView removeFromSuperview];) an/or animations are fully completed (all of CATransaction, [UIViev beginAnimation...], [UIView animateWithDuration...] etc.).
please do the following:
1- debug the for (UIView *subView in [contentVc subviews]) and check how many times it iterate.
if it doesn't crash in the first hit you can add this line before you remove the view
if (subView.superView != nil)
else try to make sure that you are not releasing the views twice somewhere else as it's
keep showing and will not crash till you remove it from it's superview.
UPDATE2:
i will consider that you are will aware of memory leaks, and that you have good experience in it.
whenever you add a subview to a view, this will retain the object by 1 in addition to the original 1, that will equal 2, then directly after adding the subview you have to release it, which will decrement the retain count back to one. here is the trick: you don't have to release the subview or remove it from it's parent view to get rid of the remaining retain count. you can simply remove or release the parent view. get the NSMutableArray As an example.
remove the [mainView removeFromSuperview]; from the dealloc: method. you can add it else where like viewWillDisappear: method. dealloc method shouldn't contain anything other than the release calls.
Instead of:
for (UIView *subView in subviews)
[subView removeFromSuperview];
Try:
[subviews makeObjectsPerformSelector:#selector(#"removeFromSuperview");
Try checking if the view is != nil first before removeFromSuperview
example:
#IBOutlet weak var btnSNSApple: UIView!
if self.btnSNSApple != nil {
self.btnSNSApple.removeFromSuperview()
}

UIRefreshControl Inside of UITableView Causing App to Freeze on Rotation - iOS 6+

Ok StackOverflow People...I've got a very interesting problem that I've been trying to solve for days and can't figure out so I need some major help. This will most likely be a very lengthy description but please bear with me and thank you deeply in advance for reading all of this because the more words I have, the clearer I can describe the full picture to you all. I will do my absolute best to be as terse and coherent as I can possibly be. Please let me know wherever I fall short.
Here's the context of my problem: I'm using Storyboards for my iOS app and for a particular nav tab in my app, I had to create two separate scenes for both the Portrait and Landscape orientations. The reason for doing this (instead of say, using Autolayout), is because within this said tab, there are visual elements (table views, web views, etc.) that are laid out differently depending on the orientation and it was a lot easier to create a separate orientation scene to handle this change in the UI instead of doing it programmatically -- (it's also just a lot easier to understand and cleaner code-wise). So the take away to keep in mind from all of this is that these two separate Portrait and Landscape scenes represent the SAME TAB in my app. (Side Note: these scenes were made in the IB of course)
Now the visual elements that I mentioned in the UI earlier -- going deeper, they are all containers for different UIViewControllers. I sandboxed everything in the app and pretty much have a 1-to-1 relationship for all things so these containers will map to my subclassed UIViewControllers that I've created for their specific purposes -- but it's here that the first caveat of my problem arises. Here's a practical example for a clearer picture, I have one UIViewController that contains a UITableView called MXSAnnouncementsViewController and this same view controller exists in both the Landscape and Portrait scenes. I did not create an explicit Portrait or Landscape VERSION of that view controller but instead, have the controller keep track of two IBOutlet properties (tableViewLandscape and tableViewPortrait) that point to the orientation-specific UITableViews -- and this approach works perfectly fine. Moreover in my MXSAnnouncementsViewController, I have a local property called tableView that abstracts the orientation-specific table views. It gets set within viewDidLoad which you can see below:
- (void)viewDidLoad
{
[super viewDidLoad];
if (self.tableViewPortrait) {
self.tableView = self.tableViewPortrait;
} else {
self.tableView = self.tableViewLandscape;
}
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
if (![MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements) {
[MXSAnnouncementManager loadModel:#"MXSAnnouncementGroupAllAnnouncements" withBlock:^(id model, NSError *error) {
if (!error) {
self.arrayLatestAnnouncements = [MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements;
[self.tableView reloadData];
} else {
// show some error msg
}
}];
} else {
self.arrayLatestAnnouncements = [MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements;
}
[self setupPullToRefresh];
}
Whenever I'm in the tab, one of the two orientation-specific IBOutlets is always active and has an address in memory while the other is nil. Whenever I rotate, the roles reverse -- whatever had an address in memory previously is now nil and the other has been initialized and allocated which is why I do what I did with the tableView property in the snippet above. Here is where caveat #2 comes into the picture and it's a doozy -- it has to do with the view lifecycle. Here's a practical example for clarity sake: Say I load the app up in Landscape orientation. When I do, my tableViewLandscape outlet has an address in memory and my tableViewPortrait outlet is nil. That's the expected and desired behavior. Now, when I rotate the app, the crazy stuff begins. Here's one place where I need clarity from all of you with regards to instances of UIViewControllers and what's normal vs. what's not so read the following VERY slowly and carefully.
Rotating the app immediately causes the opposite orientation scene (another INSTANCE of MXSAnnouncementsViewController???) to call its viewDidLoad method (in this example, we're in Landscape so the Portrait scene invokes that method). In that method, my local tableView property gets set to the currently active table view for that orientation (see snippet above). When that method finishes, the previous LANDSCAPE instance of MXSAnnouncementsViewController invokes its viewWillDisappear method which is then followed by the PORTRAIT instance's invocation of its viewWillAppear method which then lastly ends with the LANDSCAPE instance calling its willRotateToInterfaceOrientation callback -- that's the order of operation that I'm seeing from the breakpoints. I really do hope you got all of that because my mind just blew up from it all.
If you're still with me at this point, thank you because we're finally at the home stretch. As the title of this post suggests, the problem I'm trying to solve is my app freezing on rotation. If you haven't noticed on the viewDidLoad snippet, the last instruction to get executed is the setupPullToRefresh method which is the following:
- (void)setupPullToRefresh
{
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refreshTableView:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
}
Since I already explained the whole view lifecycle order of operations on rotation earlier, to make a very long story short, if I comment out that last setupPullToRefresh instruction at the end of viewDidLoad for MXSAnnouncementsViewController, my app works fine. If I include that instruction, my app becomes totally unresponsive on the first rotation and I cannot for the life of me figure out why. Not sure if I'm dealing with an edge case here or something. Any and all insights are welcome and THANK YOU SO MUCH for reading all of this!
Your best approach is probably to abandon your current design of having two separate controllers for portrait and landscape. On iOS, you should always relayout the views for the orientation you want to be in, not destroying and recreating everything. By trying to handle it by recreating everything, you're just going to get yourself in trouble I think.
You can use auto layout to do complex reorderings of views upon rotation if you know it well, but probably your best bet is to scrap your current code to do landscape, and write code to simply rearrange the views yourself upon rotating. You'll have far fewer issues down the road, and your code will be easier for others to understand and maintain as well.
When you remove that one bit of code, your app may appear to be working just fine, but there is probably something going on behind the scenes that isn't quite correct that could come back to bite you in the future. That's probably why adding the line of code breaks it.
Try to add it after rotation
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
[self setupPullToRefresh];
}
If that doesn't help, create UIRefreshControl only once and set it to the right table on rotation.
If that doesn't help too, follow the first given answer (#Gavin's answer) and create only 1 table on viewDidLoad and relayout things in -(void)viewWillLayoutSubviews

iOS 6 UIScrollview: Help My Taco App Keeps Crashing

I have a UIScrollView full of tacos.
I attached a pull-to-refresh handler to it via: https://github.com/samvermette/SVPullToRefresh
It extends uiscrollview, and exposes this method:
[scrollview addInfiniteScrollingWithActionHandler:^{
// Get me more tacos
}];
When InfiniteScrolling is triggered, I clear the scrollview's subviews and data array(intentionally) and replace it with a new set.
It works great the first time. However, when I want to load more tacos it crashes.
I get:
-[SVInfiniteScrollingView retain]: message sent to deallocated instance 0x1e5db5d0
Not surprisingly, if I leave 1 subview left in my UIScrollview, everything works fine.
Question: How can I fix this?
I thought about declaring my properties with a strong pointer, like:
#property (strong, nonatomic) IBOutlet tacoScroller *tacoScroller;
But, I worry about a retain cycle & it also doesn't work.
Any help would be appreciated, perhaps I'm missing something fundamental.
Edit:
I'm using ARC
Use an UITableView to show your tacos, this way you will reuse views and avoid wasting memory. Also it is the easiest and most convenient way to show a list of things.
One simplest solution according to your description is simply add an empty & hidden subview inside the scrollview, i am sure it wont occupy much amount of memory.
I think you are invoking the wrong method. Infinite scrolling is for other purposes.
You probably want to use
[scrollView addPullToRefreshWithActionHandler:^{ ... }];
Also, as others pointed out, you definitely should consider to use a UITableView to present your data, which seems to be very suitable for the task.

Resources