Files
AsyncDisplayKit/guide/2/index.html
2014-11-17 16:11:53 -08:00

307 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta property="og:title" content="Custom nodes &mdash; AsyncDisplayKit">
<meta property="og:type" content="website">
<meta property="og:url" content="http://asyncdisplaykit.org/guide/2/">
<meta property="og:image" content="http://asyncdisplaykit.org/assets/logo-square.png">
<meta property="og:description" content="Smooth asynchronous user interfaces for iOS apps">
<title>Custom nodes &mdash; AsyncDisplayKit</title>
<meta name="description" content="Smooth asynchronous user interfaces for iOS apps.">
<link rel="stylesheet" href="/css/main.css">
<link rel="canonical" href="http://asyncdisplaykit.org/guide/2/">
<script>
if (location.host == "facebook.github.io") {
// get outta here
location = 'http://asyncdisplaykit.org';
}
</script>
</head>
<body>
<header class="site-header">
<div class="wrapper">
<a class="site-title" href="/">AsyncDisplayKit</a>
<nav class="site-nav">
<a href="#" class="menu-icon">
<svg viewBox="0 0 18 15">
<path fill="#424242" d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/>
<path fill="#424242" d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/>
<path fill="#424242" d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/>
</svg>
</a>
<div class="trigger">
<a class="page-link page-link-active" href="/guide">guide</a>
<a class="page-link" href="/appledoc">api</a>
<a class="page-link" href="https://github.com/facebook/AsyncDisplayKit">github</a>
</div>
</nav>
</div>
</header>
<div class="page-content">
<div class="wrapper">
<div class="post">
<header class="post-header">
<h1 class="post-title">
Custom nodes
<a class="edit-page-link" href="https://github.com/facebook/AsyncDisplayKit/tree/master/docs/guide/2-custom-nodes.md" target="_blank">[edit]</a>
</h1>
</header>
<article class="post-content">
<h2>View hierarchies</h2>
<p>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:</p>
<div class="highlight"><pre><code class="language-objective-c" data-lang="objective-c"><span class="p">-</span> <span class="p">(</span><span class="bp">CGSize</span><span class="p">)</span><span class="nf">sizeThatFits:</span><span class="p">(</span><span class="bp">CGSize</span><span class="p">)</span><span class="nv">size</span>
<span class="p">{</span>
<span class="c1">// size the image</span>
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_imageView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">size</span><span class="p">];</span>
<span class="c1">// size the text view</span>
<span class="bp">CGSize</span> <span class="n">maxTextSize</span> <span class="o">=</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">size</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_textView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">maxTextSize</span><span class="p">];</span>
<span class="c1">// make sure everything fits</span>
<span class="n">CGFloat</span> <span class="n">minHeight</span> <span class="o">=</span> <span class="n">MAX</span><span class="p">(</span><span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">,</span> <span class="n">textSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="k">return</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">minHeight</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">layoutSubviews</span>
<span class="p">{</span>
<span class="bp">CGSize</span> <span class="n">size</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">;</span> <span class="c1">// convenience</span>
<span class="c1">// size and layout the image</span>
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_imageView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">size</span><span class="p">];</span>
<span class="n">_imageView</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">CGRectMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="mf">0.0f</span><span class="p">,</span>
<span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="c1">// size and layout the text view</span>
<span class="bp">CGSize</span> <span class="n">maxTextSize</span> <span class="o">=</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">size</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_textView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">maxTextSize</span><span class="p">];</span>
<span class="n">_textView</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="p">(</span><span class="bp">CGRect</span><span class="p">){</span> <span class="n">CGPointZero</span><span class="p">,</span> <span class="n">textSize</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>
<p>This isn&#39;t ideal. We&#39;re sizing our subviews twice &mdash; once to figure out
how big our view needs to be and once when laying it out &mdash; and while our
layout arithmetic is cheap and quick, we&#39;re also blocking the main thread on
expensive text sizing.</p>
<p>We could improve the situation by manually cacheing our subviews&#39; sizes, but
that solution comes with its own set of problems. Just adding <code>_imageSize</code> and
<code>_textSize</code> ivars wouldn&#39;t be enough: for example, if the text were to change,
we&#39;d need to recompute its size. The boilerplate would quickly become
untenable.</p>
<p>Further, even with a cache, we&#39;ll still be blocking the main thread on sizing
<em>sometimes</em>. We could try to shift sizing to a background thread with
<code>dispatch_async()</code>, but even if our own code is thread-safe, UIView methods are
documented to <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/index.html">only work on the main
thread</a>:</p>
<blockquote>
<p>Manipulations to your applications 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.</p>
</blockquote>
<p>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&#39;s a
laborious duplication of work. Further, if UITextView&#39;s layout behaviour
changes in an iOS update, our sizing code will break. (And did we mention that
TextKit isn&#39;t thread-safe either?)</p>
<h2>Node hierarchies</h2>
<p>Enter AsyncDisplayKit. Our custom node looks like this:</p>
<div class="highlight"><pre><code class="language-objective-c" data-lang="objective-c"><span class="cp">#import &lt;AsyncDisplayKit/AsyncDisplayKit+Subclasses.h&gt;</span>
<span class="p">...</span>
<span class="c1">// perform expensive sizing operations on a background thread</span>
<span class="o">-</span> <span class="p">(</span><span class="bp">CGSize</span><span class="p">)</span><span class="nl">calculateSizeThatFits</span><span class="p">:(</span><span class="bp">CGSize</span><span class="p">)</span><span class="n">constrainedSize</span>
<span class="p">{</span>
<span class="c1">// size the image</span>
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_imageNode</span> <span class="nl">measure</span><span class="p">:</span><span class="n">constrainedSize</span><span class="p">];</span>
<span class="c1">// size the text node</span>
<span class="bp">CGSize</span> <span class="n">maxTextSize</span> <span class="o">=</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">constrainedSize</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span>
<span class="n">constrainedSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_textNode</span> <span class="nl">measure</span><span class="p">:</span><span class="n">maxTextSize</span><span class="p">];</span>
<span class="c1">// make sure everything fits</span>
<span class="n">CGFloat</span> <span class="n">minHeight</span> <span class="o">=</span> <span class="n">MAX</span><span class="p">(</span><span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">,</span> <span class="n">textSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="k">return</span> <span class="nf">CGSizeMake</span><span class="p">(</span><span class="n">constrainedSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">minHeight</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// do as little work as possible in main-thread layout</span>
<span class="o">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">layout</span>
<span class="p">{</span>
<span class="c1">// layout the image using its cached size</span>
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="n">_imageNode</span><span class="p">.</span><span class="n">calculatedSize</span><span class="p">;</span>
<span class="n">_imageNode</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">CGRectMake</span><span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="mf">0.0f</span><span class="p">,</span>
<span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
<span class="c1">// layout the text view using its cached size</span>
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="n">_textNode</span><span class="p">.</span><span class="n">calculatedSize</span><span class="p">;</span>
<span class="n">_textNode</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="p">(</span><span class="bp">CGRect</span><span class="p">){</span> <span class="n">CGPointZero</span><span class="p">,</span> <span class="n">textSize</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>
<p>ASImageNode and ASTextNode, like the rest of AsyncDisplayKit, are thread-safe,
so we can size them on background threads. The <code>-measure:</code> method is like
<code>-sizeThatFits:</code>, but with side effects: it caches both the argument
(<code>constrainedSizeForCalculatedSize</code>) and the result (<code>calculatedSize</code>) for
quick access later on &mdash; like in our now-snappy <code>-layout</code> implementation.</p>
<p>As you can see, node hierarchies are sized and laid out in much the same way as
their view counterparts. Custom nodes do need to be written with a few things
in mind:</p>
<ul>
<li><p>Nodes must recursively measure all of their subnodes in their
<code>-calculateSizeThatFits:</code> implementations. Note that the <code>-measure:</code>
machinery will only call <code>-calculateSizeThatFits:</code> if a new measurement pass
is needed (e.g., if the constrained size has changed).</p></li>
<li><p>Nodes should perform any other expensive pre-layout calculations in
<code>-calculateSizeThatFits:</code>, cacheing useful intermediate results in ivars as
appropriate.</p></li>
<li><p>Nodes should call <code>[self invalidateCalculatedSize]</code> when necessary. For
example, ASTextNode invalidates its calculated size when its
<code>attributedString</code> property is changed.</p></li>
</ul>
<p>For more examples of custom sizing and layout, along with a demo of
ASTextNode&#39;s features, check out <code>BlurbNode</code> and <code>KittenNode</code> in the
<a href="https://github.com/facebook/AsyncDisplayKit/tree/master/examples/Kittens">Kittens</a>
sample project.</p>
<h2>Custom drawing</h2>
<p>To guarantee thread safety in its highly-concurrent drawing system, the node
drawing API diverges substantially from UIView&#39;s. Instead of implementing
<code>-drawRect:</code>, you must:</p>
<ol>
<li><p>Define an internal &quot;draw parameters&quot; class for your custom node. This
class should be able to store any state your node needs to draw itself
&mdash; it can be a plain old NSObject or even a dictionary.</p></li>
<li><p>Return a configured instance of your draw parameters class in
<code>-drawParametersForAsyncLayer:</code>. This method will always be called on the
main thread.</p></li>
<li><p>Implement either <code>+drawRect:withParameters:isCancelled:isRasterizing:</code> or
<code>+displayWithParameters:isCancelled:</code>. Note that these are <em>class</em> methods
that will not have access to your node&#39;s state &mdash; only the draw
parameters object. They can be called on any thread and must be
thread-safe.</p></li>
</ol>
<p>For example, this node will draw a rainbow:</p>
<div class="highlight"><pre><code class="language-objective-c" data-lang="objective-c"><span class="k">@interface</span> <span class="nc">RainbowNode</span> : <span class="nc">ASDisplayNode</span>
<span class="k">@end</span>
<span class="k">@implementation</span> <span class="nc">RainbowNode</span>
<span class="p">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">drawRect:</span><span class="p">(</span><span class="bp">CGRect</span><span class="p">)</span><span class="nv">bounds</span>
<span class="nf">withParameters:</span><span class="p">(</span><span class="kt">id</span><span class="o">&lt;</span><span class="bp">NSObject</span><span class="o">&gt;</span><span class="p">)</span><span class="nv">parameters</span>
<span class="nf">isCancelled:</span><span class="p">(</span><span class="kt">asdisplaynode_iscancelled_block_t</span><span class="p">)</span><span class="nv">isCancelledBlock</span>
<span class="nf">isRasterizing:</span><span class="p">(</span><span class="kt">BOOL</span><span class="p">)</span><span class="nv">isRasterizing</span>
<span class="p">{</span>
<span class="c1">// clear the backing store, but only if we&#39;re not rasterising into another layer</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isRasterizing</span><span class="p">)</span> <span class="p">{</span>
<span class="p">[[</span><span class="bp">UIColor</span> <span class="n">whiteColor</span><span class="p">]</span> <span class="n">set</span><span class="p">];</span>
<span class="n">UIRectFill</span><span class="p">(</span><span class="n">bounds</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// UIColor sadly lacks +indigoColor and +violetColor methods</span>
<span class="bp">NSArray</span> <span class="o">*</span><span class="n">colors</span> <span class="o">=</span> <span class="l">@[</span> <span class="p">[</span><span class="bp">UIColor</span> <span class="n">redColor</span><span class="p">],</span>
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">orangeColor</span><span class="p">],</span>
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">yellowColor</span><span class="p">],</span>
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">greenColor</span><span class="p">],</span>
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">blueColor</span><span class="p">],</span>
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">purpleColor</span><span class="p">]</span> <span class="l">]</span><span class="p">;</span>
<span class="n">CGFloat</span> <span class="n">stripeHeight</span> <span class="o">=</span> <span class="n">roundf</span><span class="p">(</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">height</span> <span class="o">/</span> <span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">colors</span><span class="p">.</span><span class="n">count</span><span class="p">);</span>
<span class="c1">// draw the stripes</span>
<span class="k">for</span> <span class="p">(</span><span class="bp">UIColor</span> <span class="o">*</span><span class="n">color</span> <span class="k">in</span> <span class="n">colors</span><span class="p">)</span> <span class="p">{</span>
<span class="bp">CGRect</span> <span class="n">stripe</span> <span class="o">=</span> <span class="n">CGRectZero</span><span class="p">;</span>
<span class="n">CGRectDivide</span><span class="p">(</span><span class="n">bounds</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">stripe</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">bounds</span><span class="p">,</span> <span class="n">stripeHeight</span><span class="p">,</span> <span class="n">CGRectMinYEdge</span><span class="p">);</span>
<span class="p">[</span><span class="n">color</span> <span class="n">set</span><span class="p">];</span>
<span class="n">UIRectFill</span><span class="p">(</span><span class="n">stripe</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">@end</span>
</code></pre></div>
<p>This could easily be extended to support vertical rainbows too, by adding a
<code>vertical</code> property to the node, exporting it in
<code>-drawParametersForAsyncLayer:</code>, and referencing it in
<code>+drawRect:withParameters:isCancelled:isRasterizing:</code>. More-complex nodes can
be supported in much the same way.</p>
<p>For more on custom nodes, check out the <a href="https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/ASDisplayNode%2BSubclasses.h">subclassing
header</a>
or read on!</p>
</article>
<div class="docs-prevnext">
<a class="docs-prev" href="/guide/">&larr; prev</a>
<a class="docs-next" href="/guide/3/">next &rarr;</a>
</div>
</div>
</div>
</div>
<footer class="site-footer">
<div class="wrapper">
<div class="footer-col-wrapper">
<div class="footer-col footer-col-left">
<p class="text">a Facebook &amp; Instagram collaboration &#x2665;</p>
</div>
<div class="footer-col footer-col-right">
<p class="text">
&copy; 2014 Facebook Inc (<a href="/license/">CC-BY-4.0</a>)
</p>
</div>
</div>
</div>
</footer>
</body>
</html>