Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Layout at Scale with AsyncDisplayKit 2

Layout at Scale with AsyncDisplayKit 2

AsyncDisplayKit 2 introduces a declarative layout API for writing complex, adaptive user interfaces that remain smooth and responsive. Through familiar paradigms like CSS flexbox, the API provides a simple way to describe expressive, composable layouts that adapt to different screen sizes and are fully animatable. In this talk, we’ll take a deep dive into how the layout API works, how Pinterest put it to the test at scale, and how you can implement your own apps with it.

Levi McCallum is the manager of iOS Core Experience at Pinterest — the team working on iOS core platform and frameworks. He is a core contributor to AsyncDisplayKit and deeply passionate about building frameworks that make building fast apps effortless and scalable. Follow him on Twitter @levi.

Originally presented at NSMeetup in SF

Levi McCallum

October 12, 2016
Tweet

Other Decks in Programming

Transcript

  1. Move CPU & IO bound work off the main thread

    Core Optimizations Basic principles of AsyncDisplayKit
  2. Move CPU & IO bound work off the main thread

    Perform layout and drawing work ahead of time Core Optimizations Basic principles of AsyncDisplayKit
  3. Move CPU & IO bound work off the main thread

    Perform layout and drawing work ahead of time Retain familiarity with UIKit Core Optimizations Basic principles of AsyncDisplayKit
  4. Display Node Enable layout and drawing on the background Take

    advantage of multiple cores Nodes represent a view or layer
  5. Display Node Enable layout and drawing on the background Take

    advantage of multiple cores Nodes represent a view or layer Display Node .view View Layer .layer
  6. Display Node Enable layout and drawing on the background Take

    advantage of multiple cores Nodes represent a view or layer Build entire hierarchies
  7. PinNode class PinNode: ASCellNode { let imageNode = ASNetworkImageNode() let

    titleNode = ASTextNode() let fromNode = ASTextNode() init(pin: PinModel) { super.init() imageNode.url = pin.imageURL titleNode.attributedText = pin.attributedTitle fromNode.attributedText = pin.attributedFrom addSubnodes(imageNode, titleNode, fromNode) } }
  8. PinNode let node = PinNode(PinModel({ … })) node.frame = CGRect(

    x: 0, y: 0, width: 150, height: 300 ) self.view.addSubview(pin.view)
  9. Designed to feel familiar - sizeThatFits: & layoutSubviews Two passes

    Display Node Layout Before AsyncDisplayKit 2.0
  10. Measure Pass let node = PinNode(pin) self.view.addSubview(pin.view) let size =

    node.measure(CGSize(width: 160, height: 250)) node.frame = CGRect(origin: .zero, size: size) Before AsyncDisplayKit 2
  11. override func calculateSizeThatFits(constrainedSize: CGSize) -> CGSize { let imageSize =

    imageNode.measure(constrainedSize) var totalHeight = imageSize.height var insetSize = constrainedSize insetSize.width = insetSize.width - 20 - 20 let fromSize = fromNode.measure(insetSize) totalHeight += 20 + fromSize.height let titleSize = titleNode.measure(insetSize) totalHeight += 20 + titleSize.height return CGSize(width: constrainedSize.width, height: totalHeight) }
  12. override func layout() { imageNode.frame = CGRect(origin: .zero, size: imageNode.calculatedSize)‚Àè

    var fromOrigin = imageNode.frame.origin fromOrigin.y += imageNode.frame.size.height + 20 fromNode.frame = CGRect(origin: fromOrigin, size: fromNode.calculatedSize) var titleOrigin = fromNode.frame.origin titleOrigin.y += fromOrigin.size.height + 20 titleNode.frame = CGRect(origin: titleOrigin, size: titleNode.calculatedSize) } Layout Pass Before AsyncDisplayKit 2
  13. Node Containers Don’t trigger measure yourself Simple entry point of

    display nodes in your app ASCollectionView / ASTableView / ASViewController Intelligent preloading
  14. Node Containers Don’t trigger measure yourself Simple entry point of

    display nodes in your app ASCollectionView / ASTableView / ASViewController Intelligent preloading
  15. Node Containers Don’t trigger measure yourself Simple entry point of

    display nodes in your app ASCollectionView / ASTableView / ASViewController Intelligent preloading
  16. Node Containers Don’t trigger measure yourself Simple entry point of

    display nodes in your app ASCollectionView / ASTableView / ASViewController Intelligent preloading
  17. PinNode func collectionView(collectionView: ASCollectionView, nodeBlockForItemAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock {

    let pin = pins[indexPath.item] return { return PinNode(pin) } } ASCollectionView Delegate Implementation
  18. override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { return ASStackLayoutSpec( direction:

    .Vertical, spacing: 20.0, justifyContent: .Start, alignItems: .Start, children: [ imageNode, fromNode, titleNode ] ) }
  19. override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize = CGSize(width:

    150.0, height: 150.0) return ASStackLayoutSpec( direction: .Vertical, spacing: 20.0, justifyContent: .Start, alignItems: .Start, children: [ imageNode, fromNode, titleNode ] ) }
  20. override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { let ratio =

    ASRatioSpec(ratio: 1.0, imageNode) return ASStackLayoutSpec( direction: .Vertical, spacing: 20.0, justifyContent: .Start, alignItems: .Start, children: [ ratio, fromNode, titleNode ] ) }
  21. override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { let ratio =

    ASRatioSpec(ratio: 1.0, imageNode) let inset = ASInsetLayoutSpec( insets: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20), child: ? ) return ASStackLayoutSpec( direction: .Vertical, spacing: 20.0, justifyContent: .Start, alignItems: .Start, children: [ ratio, inset ] ) }
  22. override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { let ratio =

    ASRatioSpec(ratio: 1.0, imageNode) let inset = ASInsetLayoutSpec( insets: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20), child: ASStackLayoutSpec( direction: .Vertical, spacing: 20.0, justifyContent: .Start, alignItems: .Start, children: [ fromNode, titleNode ] ) ) return ASStackLayoutSpec( direction: .Vertical, spacing: 20.0, justifyContent: .Start, alignItems: .Start, children: [ ratio, inset ] ) }
  23. Layout Objects Immutable representation of position and size Cache and

    reuse for layout passes Size Position ASLayout Display Node .calculatedLayout .layoutElement
  24. Layout Tree Layout objects are hierarchical Describe subnode frames Size

    Sublayouts ASLayout Size Position ASLayout Size Position ASLayout
  25. Layout Tree Layout objects are hierarchical Describe subnode frames Used

    in the next layout pass Size Sublayouts ASLayout Size Position ASLayout Size Position ASLayout
  26. Layout Pass AsyncDisplayKit 2.0 Completely automatic 1 func layout() {

    for layout in self.calculatedLayout.sublayouts { if let subnode = layout.element as? ASDisplayNode { subnode.frame = layout.frame } } }
  27. class ASInsetSpec: ASLayoutSpec { var insets: UIEdgeInsets override func layoutThatFits(constrainedSize:

    ASSizeRange) -> ASLayout { return ASLayout( layoutElement: self, constrainedSizeRange: ?, sublayouts: [?] ) } } Inside Inset Spec
  28. override func layoutThatFits(constrainedSize: ASSizeRange) -> ASLayout { let childSublayout =

    self.child.layoutThatFits(insetConstrainedSize) childSublayout.position = CGPoint(x: insets.left, y: insets.top) return ASLayout( layoutElement: self, constrainedSizeRange: ?, sublayouts: [childSublayout] ) }
  29. override func layoutThatFits(constrainedSize: ASSizeRange) -> ASLayout { let insetsX =

    insets.left + insets.right; let insetsY = insets.top + insets.bottom; let insetConstrainedSize = ASSizeRange( min: CGSize(width: constrainedSize.min.width - insetsX, height: constrainedSize.min.height - insetsY), max: CGSize(width: constrainedSize.max.width - insetsX, height: constrainedSize.max.height - insetsY), ) let childSublayout = self.child.layoutThatFits(insetConstrainedSize) childSublayout.position = CGPoint(x: insets.left, y: insets.top) return ASLayout( layoutElement: self, constrainedSizeRange: ?, sublayouts: [childSublayout] ) }
  30. let insetConstrainedSize = ASSizeRange( min: CGSize(width: constrainedSize.min.width - insetsX, height:

    constrainedSize.min.height - insetsY), max: CGSize(width: constrainedSize.max.width - insetsX, height: constrainedSize.max.height - insetsY), ) let childSublayout = self.child.layoutThatFits(insetConstrainedSize) childSublayout.position = CGPoint(x: insets.left, y: insets.top) let computedSize = ASSizeRange(exactSize: CGSize( width: childSublayout.size.width + insets.left + insets.right, height: childSublayout.size.height + insets.top + insets.bottom )) return ASLayout( layoutElement: self, constrainedSizeRange: computedSize, sublayouts: [childSublayout] ) }
  31. Direct Frame Animation Animate underlying views directly Large amount of

    boilerplate Managing subnode hierarchy was cumbersome
  32. LayoutSpec Transitions Animate between layout spec definitions override func layoutSpecThatFits(constrainedSize:

    ASSizeRange) -> ASLayoutSpec { let field = fieldNode(forState: fieldState) let stack = ASStackLayoutSpec() stack.children = [labelNode, field, progressNode, buttonNode] return stack }
  33. LayoutSpec Transitions Animate between layout spec definitions func onPressedNext() {

    signupNode.fieldState = .Name signupNode.transitionLayout(animated: true) }
  34. LayoutSpec Transitions Compares old and new layout tree Animates visual

    changes with default • Fades out and removes old nodes • Fades in and inserts new nodes
  35. LayoutSpec Transitions Compares old and new layout tree Animates visual

    changes with default • Fades out and removes old nodes • Fades in and inserts new nodes Opt-in via Display Node property self.automaticallyManagesSubnodes = true
  36. Auto Managed Subnodes No longer need to call addSubnode or

    removeFromSupernode Insertions and deletions determined by LCS diffing of old & new layout trees
  37. Auto Managed Subnodes Not just for animations The layout spec

    is the description for what’s in the UI signupNode.fieldState = .Password signupNode.setNeedsLayout()