
git-subtree-dir: submodules/AsyncDisplayKit git-subtree-mainline: d06f423e0ed3df1fed9bd10d79ee312a9179b632 git-subtree-split: 02bedc12816e251ad71777f9d2578329b6d2bef6
7.6 KiB
Executable File
title | layout | permalink |
---|---|---|
Manual Layout | docs | /docs/layout2-manual-layout.html |
Manual Layout
After diving in to the automatic way for layout in Texture there is still the old way to layout manually available. For the sake of completness here is a short description how to accomplish that within Texture.
Manual Layout UIKit
Sizing and layout of custom view hierarchies are typically done all at once on the main thread. For example, a custom UIView that minimally encloses a text view and an image view might look like this:
- (CGSize)sizeThatFits:(CGSize)size
{
// size the image
CGSize imageSize = [_imageView sizeThatFits:size];
// size the text view
CGSize maxTextSize = CGSizeMake(size.width - imageSize.width, size.height);
CGSize textSize = [_textView sizeThatFits:maxTextSize];
// make sure everything fits
CGFloat minHeight = MAX(imageSize.height, textSize.height);
return CGSizeMake(size.width, minHeight);
}
-
(void)layoutSubviews { CGSize size = self.bounds.size; // convenience
// size and layout the image CGSize imageSize = [_imageView sizeThatFits:size]; _imageView.frame = CGRectMake(size.width - imageSize.width, 0.0f, imageSize.width, imageSize.height);
// size and layout the text view CGSize maxTextSize = CGSizeMake(size.width - imageSize.width, size.height); CGSize textSize = [_textView sizeThatFits:maxTextSize]; _textView.frame = (CGRect){ CGPointZero, textSize }; }
This isn't ideal. We're sizing our subviews twice — once to figure out how big our view needs to be and once when laying it out — and while our layout arithmetic is cheap and quick, we're also blocking the main thread on expensive text sizing.
We could improve the situation by manually cacheing our subviews' sizes, but that solution comes with its own set of problems. Just adding _imageSize
and _textSize
ivars wouldn't be enough: for example, if the text were to change, we'd need to recompute its size. The boilerplate would quickly become untenable.
Further, even with a cache, we'll still be blocking the main thread on sizing sometimes. We could try to shift sizing to a background thread with dispatch_async()
, but even if our own code is thread-safe, UIView methods are documented to only work on the main thread:
Manipulations to your application’s user interface must occur on the main thread. Thus, you should always call the methods of the UIView class from code running in the main thread of your application. The only time this may not be strictly necessary is when creating the view object itself but all other manipulations should occur on the main thread.
This is a pretty deep rabbit hole. We could attempt to work around the fact that UILabels and UITextViews cannot safely be sized on background threads by manually creating a TextKit stack and sizing the text ourselves... but that's a laborious duplication of work. Further, if UITextView's layout behaviour changes in an iOS update, our sizing code will break. (And did we mention that TextKit isn't thread-safe either?)
Manual Layout Texture
Manual layout within Texture are realized within two methods:
calculateSizeThatFits
and layout
Within calculateSizeThatFits:
you should provide a intrinsic content size for the node based on the given constrainedSize
. This method is called on a background thread so perform expensive sizing operations within it.
- [ASDisplayNode calculateSizeThatFits:]
After measurement and layout pass happens further layout can be done in layout
. This method is called on the main thread. In there, layout operations can be done for nodes that are not playing within the automatic layout system and are referenced within layoutSpecThatFits:
.
- [ASDisplayNode layout]
Example
Our custom node looks like this:
#import <AsyncDisplayKit/AsyncDisplayKit+Subclasses.h>
...
// perform expensive sizing operations on a background thread
-
(CGSize)calculateSizeThatFits:(CGSize)constrainedSize { // size the image CGSize imageSize = [_imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size;
// size the text node CGSize maxTextSize = CGSizeMake(constrainedSize.width - imageSize.width, constrainedSize.height);
CGSize textSize = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, maxTextSize)].size;
// make sure everything fits CGFloat minHeight = MAX(imageSize.height, textSize.height); return CGSizeMake(constrainedSize.width, minHeight); }
// do as little work as possible in main-thread layout
-
(void)layout { // layout the image using its cached size CGSize imageSize = _imageNode.calculatedSize; _imageNode.frame = CGRectMake(self.bounds.size.width - imageSize.width, 0.0f, imageSize.width, imageSize.height);
// layout the text view using its cached size CGSize textSize = _textNode.calculatedSize; _textNode.frame = (CGRect){ CGPointZero, textSize }; }
ASImageNode
and ASTextNode
, like the rest of Texture, are thread-safe, so we can size them on background threads. The -layoutThatFits:
method is like -sizeThatFits:
, but with side effects: it caches the (calculatedSize
) for quick access later on — like in our now-snappy -layout
implementation.
As you can see, node hierarchies are sized and laid out in much the same way as their view counterparts. Manually layed out nodes do need to be written with a few things in mind:
-
Nodes must recursively measure all of their subnodes in their
-calculateSizeThatFits:
implementations. Note that the-layoutThatFits:
machinery will only call-calculateSizeThatFits:
if a new measurement pass is needed (e.g., if the constrained size has changed) andlayoutSpecThatFits:
is not implemented. -
Nodes should perform any other expensive pre-layout calculations in
-calculateSizeThatFits:
, caching useful intermediate results in ivars as appropriate. -
Nodes should call
[self invalidateCalculatedSize]
when necessary. For example,ASTextNode
invalidates its calculated size when itsattributedString
property is changed.
As already mentioned, automatic layout is preferred over manual layout and should be the way to go in most cases.