UIKit Dynamics - Issue with referencing the UIDynamicAnimator from ChildViewController - ios

I have one UIViewController container with contains multiple UIViewController boxes. I'd like to apply physics behaviour to the boxes using UIKit Dynamics. The container defines the outer border.
The UIDynamicAnimator is an object in the container and all Dynamics behaviours are part of the boxes.
Problem: I run into error messages when I reference the animator from the boxes (childviewcontrollers) for adding the box behaviours.
Here's the error message
> Terminating app due to uncaught exception
> 'NSInvalidArgumentException', reason: 'View item (<UIView: 0x9972720;
> frame = (0 568; 120 40); clipsToBounds = YES; autoresize = W+H;
> gestureRecognizers = <NSArray: 0x9976f80>; layer = <CALayer:
> 0x9972780>>) should be a descendant of reference view in
> <UIDynamicAnimator: 0x9906d40> Stopped (0.000000s) in <UIView:
> 0x9916bf0> {{0, 0}, {320, 568}}'
UIViewController Container
- (void)viewDidLoad
{
[super viewDidLoad];
UIDynamicAnimator* animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
_animator = animator;
...
}
Adding Boxes as Subviews and ChildViewControllers
- (void)tapOnContainer:(UITapGestureRecognizer*)touch {
...
BoxViewController *note = [[BoxViewController alloc]init];
[box setContainerObject:self];
[self addChildViewController:box];
[self.view addSubview:box.view];
}
UIViewController Box
- (void)viewDidLoad
{
_containerViewController = (ViewController*) self.parentViewController;
_gravityBehaviour = [[UIGravityBehavior alloc]initWithItems:#[self.view]];
[_containerViewController.animator addBehavior:_gravityBehaviour];
...
}
Any idea how to get this working? Is it okay to split the UIDynamicAnimator and the UIDynamicBehavior objects like this?

In this line:
_gravityBehaviour = [[UIGravityBehavior alloc]initWithItems:#[self.view]];
Add your small view instead of self.view.

Related

When to layout subviews of UIViewControllers view?

In my UIViewController class, I'm creating a UIView called safeAreaView and adding it as a subview to the UIViewControllers view property. I'm making it so safeAreaView takes up the entire safe area of the UIViewControllers view property:
- (void) viewDidLoad
{
[self setToolbarWithColor: self.mainToolbarColor animated:NO];
self.tapGestureRecognizer.delegate = self;
self.view.clipsToBounds = YES;
self.view.backgroundColor = [UIColor lightGrayColor];
self.safeAreaView = [[UIView alloc] initWithFrame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
self.safeAreaView.clipsToBounds = YES;
self.safeAreaView.delegate = self;
self.safeAreaView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview: self.safeAreaView];
[self.safeAreaView.leadingAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.leadingAnchor].active = YES;
[self.safeAreaView.trailingAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.trailingAnchor].active = YES;
[self.safeAreaView.topAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.topAnchor].active = YES;
[self.safeAreaView.bottomAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.bottomAnchor].active = YES;
[self.safeAreaView loadSubviews];
}
This works fine, but my problem is, at some point after this during the UIViewControllers initialization cycle, safeAreaView updates to account for the statusbar (it's y position moves up 20 and it decreases in size by 20).
I need to layout some subviews on safeAreaView and I don't know the proper time? If I attach the subviews like above, they have the wrong height. And I can't use some auto layout features on the subviews because there are specific things that I need to do. I've also tried executing the above code in viewWillAppear with no luck.
Wondering if anyone had any suggestions?
You can override - (void)layoutSubviews on your safeAreaView class:
- (void)layoutSubviews {
[super layoutSubviews];
// Manual frame adjustment for non-autolayout participating subviews
// ...
}
Another option would be to override your safeAreaView's class frame setter, so each time the frame of your view changes, you'll get a chance to manually set any subview frames as needed.

ScrollView and subviews don't match between code and Storyboards?

I have created a UIScrollView in my Storyboards file, and properly connected it to my controller IBOutlet attribute. Here is what it looks like in Storyboards:
UIScrollView
UIView 1
UIView 2
UIView 3
Constraints
...
This is the code I am running on viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"COUNT: %d", [self.scrollView subviews].count);
for(UIView *v in [self.scrollView subviews]) {
NSLog(#"View: %#", v);
}
}
I was, therefore, expecting to see my 3 views listed in my console log. However, it is printing something totally unrelated (2 UIImageViews?):
2014-09-17 13:06:33.155 Project[6333:652705] COUNT: 2
2014-09-17 13:06:33.156 Project[6333:652705] View: <UIImageView: 0x7b094460; frame = (233 124.5; 7 3.5); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer: 0x7b0947c0>>
2014-09-17 13:06:33.156 Project[6333:652705] View: <UIImageView: 0x7b094fb0; frame = (236.5 121; 3.5 7); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x7b095080>>
I may be clearly doing something wrong here, but is there some sort of cache in Interface Builder or the Outlet connection itself?
The image views are the scrollers at the edge of the scroll view. When viewDidLoad is called the view hierarchy hasn't been fully installed yet. Move your code into the awakeFromNib method instead (and watch out for the image views - generally don't rely on the subviews, keep explicit references to the views you want to access).

Can I use auto layout with UIPageViewController's pages?

I have the following controller stack:
- UINavigationController
- UITableViewController
- UIPageViewController
- MyTableViewController1
- MyTableViewController2
- MyTableViewController3
I am using AutoLayout and respecting the layout guides throughout the whole application.
However, MyTableViewController1|2|3 are instantiated inside the UIPageViewController DataSource methods and not through storyboard segues. I'm loading the viewControllers from storyboard using:
[self.storyboard instantiateViewControllerWithIdentifier:#"MyTableViewController1|2|3"];
MyTableViewController1|2|3.adjustScrollViewInsets = YES in storyboard.
But the adjustScrollViewInsets does nothing
In MyTableViewController1|2|3.viewDidLayoutSubViews I log the following:
self.view.frame = {{0, 0}, {320, 568}}
self.bottomLayoutGuide = <_UILayoutGuide: 0x1701b0bc0; frame = (0 531; 0 0); hidden = YES; layer = <CALayer: 0x17022fde0>>
self.topLayoutGuide = <_UILayoutGuide: 0x1781ae700; frame = (0 0; 0 64); hidden = YES; layer = <CALayer: 0x178220ca0>>
It seems I'm just missing some little detail here. Answers I found on stack-overflow are all for autolayout = NO situations.
The problem is MyTableViewController1|2|3's cells render behind the navigationBar.
Any help is much appreciated
Thanks

UIScrollView with 3 UIViewController subviews

I'm trying to create a UIScrollView with 3 UIViewController subviews. I'm done some research and it seems that the answer to this question provides a good solution Setting up UIScrollView to swipe between 3 view controllers, but I'm not sure. I downloaded the sample code which the answerer nicely posted https://github.com/gneil90/CustomContainerViewController and my question is as follows:
BViewController *bViewController = [[BViewController alloc]init];
[self addChildViewController:bViewController];
[self.scrollView addSubview:bViewController.view];
[bViewController didMoveToParentViewController:self];
CViewController *cViewController = [[CViewController alloc]init];
CGRect frame = cViewController.view.frame;
frame.origin.x = 320;
cViewController.view.frame = frame;
[self addChildViewController:cViewController];
[self.scrollView addSubview:cViewController.view];
[cViewController didMoveToParentViewController:self];
self.scrollView.contentSize = CGSizeMake(640, self.view.frame.size.height);
self.scrollView.pagingEnabled = YES;
The code in the sample project initializes both UIViewControllers at once. Is there any way to lazy load them/would this provide any kind of performance enhancements? If 2 of my viewcontrollers download data, I wouldn't want to download this data unless the user was actually viewing these screens, but with this kind of initialization it would seems as though the user quickly viewed both of them.
You can use UIScrollView delegate method, initialize only first view controller as you are doing.
For rest of your view controller you can create property. In UIScrollView delegate method scrollViewDidScroll do following-
// Check at what index, to second VC or third VC
if (some condition, you can define this based on scroll postion) {
// Check if second view controller
if (!self.cViewController) {
cViewController = [[CViewController alloc]init];
CGRect frame = cViewController.view.frame;
frame.origin.x = 320;
cViewController.view.frame = frame;
}
// similarly for third
}
Using this view controller are initialized as they are needed.

Creating an autolayout-based metrics view

I have a reusable view I will be using in UITableViewCell's and UICollectionViewCell's, and need to get its dimensions for tableView:heightForRowAtIndexPath:. Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:, instead my plan is to:
Instantiate the metrics view.
Set the size to include the desired width.
Populate it with data.
Update constraints / Layout subviews.
Grab the height of the view or an internal "sizing" view.
The problem I'm running into is that I cannot force the view to layout without inserting it into the view and waiting for the runloop.
I've distilled a rather boring example. Here's View.xib. The subview is misaligned to highlight that the view is never getting laid out even to the baseline position:
On the main thread I call:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
NSLog(#"Subviews: %#", view.subviews);
view.frame = CGRectMake(0, 0, 200, 200);
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
[self.view addSubview:view];
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Subviews: %#", view.subviews);
});
I get out the following view information:
1) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
2) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
3) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
4) "<UIView: 0x8bad9e0; frame = (0 100; 100 100); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
1 indicates that the fresh-out-of-the-NIB view hasn't been laid out. 2 indicates that updateConstraints/layoutSubviews did nothing. 3 indicates that adding it to the view hierarchy did nothing. 4 finally indicates that adding to the view hierarchy and one pass through the main-loop laid out the view.
I would like to get to the point where I can get the view's dimensions without having to let the application handle it or perform manual calculations (string height + constraint1 + constraint2) on my own.
Update
I've observed that if I place view inside a UIWindow I get a slight improvement:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
view.frame = CGRectMake(0, 0, 200, 200);
[window addSubview:view];
[view layoutSubviews];
If view.translatesAutoresizingMaskIntoConstraints == YES, the view's immediate subviews will be laid out, but none of their children.
The Autolayout Question
In the basic case you mentioned, you can get the correct size by calling setNeedsLayout and then layoutIfNeeded on the container view.
From the UIView class reference on layoutIfNeeded:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor. Therefore, calling this method can potentially force the layout of your entire view hierarchy. The UIView implementation of this calls the equivalent CALayer method and so has the same behavior as CALayer.
I don't think the "entire view hierarchy" applies to your use case since the metrics view presumably wouldn't have a superview.
Sample Code
In a sample empty project, with just this code, the correct frame is determined after layoutIfNeeded is called:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) UIView *redView;
#end
#implementation ViewController
#synthesize redView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
redView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 220, 468)];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-100-[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view setNeedsLayout];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view layoutIfNeeded];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{0, 100}, {100, 100}}"
}
#end
Additional Considerations
Slightly outside the scope of your question, here are some other issues you may run into, since I've worked on this exact problem in a real app:
Calculating this in heightForRowAtIndexPath: might be expensive, so you may want to precalculate and cache the results
Precalculation should be done on a background thread, but UIView layout doesn't work well unless it's done on the main thread
You should definitely implement estimatedHeightForRowAtIndexPath: to reduce the impact of these performance issues
Using intrinsicContentSize
In response to:
Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:
You can use this method if you implement intrinsicContentSize, which lets a view suggest an optimal size for itself. One implementation for this might be:
- (CGSize) intrinsicContentSize {
[self layoutSubviews];
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
This simple approach will only work if your layoutSubviews method doesn't refer to an already-set size (like self.bounds or self.frame). If it does, you may need to do something like:
- (CGSize) intrinsicContentSize {
self.frame = CGRectMake(0, 0, 10000, 10000);
while ([self viewIsWayTooLarge] == YES) {
self.frame = CGRectInset(self.frame, 100, 100);
[self layoutSubviews];
}
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
Obviously, you'd need to adjust these values to match the particular layout of each view, and you may need to tune for performance.
Finally, I'll add that due in part to the exponentially increasing cost of using auto-layout, for all but the simplest table cells, I usually wind up using manual height calculation.
Presumably you're calling the demo code when the view controller first loads its view, like in viewDidLoad or another life cycle method. The nested subview's geometries won't reflect its constraints until viewDidLayoutSubviews is called. Nothing you do during the initial life cycle of a view controller will make that method arrive any faster.
Update 12/30/13: After testing Aaron Brager's sample code, I now realize that the previous paragraph is incorrect. Apparently, you can force layout in viewDidLoad by calling setNeedsLayout followed by layoutIfNeeded.
If you executed the demo code in response to a button click instead, I think you'll see the final geometries of your nested subview logged before the action method completes.
- (IBAction)buttonTapped:(id)sender
{
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
view.frame = CGRectMake(0, 0, 200, 200);
[self.view addSubview:view];
[self.view layoutIfNeeded];
NSLog(#"Subviews: %#", view.subviews);
}
In the latter case, you can request layout on-demand because the view controller has completed its initial setup.
But during a view controller's initial setup, how are you going to get the final geometries of your re-usable subview?
After you set the content for the re-usable subview, have your view controller ask the subview for its size. In other words, implement a method on your custom view that calculates the size based on the content.
For example, if the subview's content is an attributed string, you could use a method like boundingRectWithSize:options:context: to help determine the size of your subview.
CGRect rect = [attributedString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsersLineFragmentOrigin context:nil];

Resources