QuickLayout

repository·main·Indexed 43 days ago

https://github.com/facebook/quicklayout

A high-performance, declarative layout library for iOS designed to replace Auto Layout and Flexbox. It provides a lightweight Swift-based engine for UIKit applications, utilizing a DSL with VStack, HStack, and Spacers to create memory-efficient layouts. The library features the @QuickLayout macro for automatic subview management and an optimization system using flexibility annotations and layoutPriority(_:) to avoid double measurement performance issues.

Tokens
23.1K
Snippets
97
Records
117
Agent score
46%

What's inside QuickLayout

  1. Core Features of QuickLayout

    main

    QuickLayout provides several key advantages for UIKit development:

    • Lightning-Fast Performance: Uses a custom layout engine that avoids the overhead of Auto Layout or Flexbox.
    • Modern Layout Primitives: Uses lightweight, pure Swift structs like HStack, VStack, ZStack, Grid, and Flow that do not add extra views to the hierarchy.
    • Easy Integration: The @QuickLayout macro automates subview management and setup.
    • Thread-Safe: The API can be used concurrently and off the main thread (refer to concepts/thread-safety/ for specific caveats).
  2. What is LazyView and how does it work?

    main

    A LazyView is a design pattern in QuickLayout used to delay the initialization of a view until it is actually required by the layout. This pattern is intended to improve startup time, scroll performance, and other business metrics by avoiding the immediate creation of views that might not be visible or needed.

    LazyView behaves similarly to lazy variables in Swift, but includes an ifLoaded getter. This getter allows you to safely access and modify the underlying view only if it has already been initialized, without triggering a new load.

    private var replyButton = LazyView { [weak self] in
      /// This closure is called when the view is used in the layout for the first time.
      let button = UIButton(type: .system)
      ...
      return button
    }
  3. Understand QuickLayout thread safety constraints

    main

    The core QuickLayout layout algorithm (including VStack, HStack, frame, and padding) is thread-safe. However, the thread safety of a complete layout operation is determined by its leaf elements.

    Critical Constraint: If your layout hierarchy contains UIView instances, you must perform layout operations on the main thread. This is because UIView methods like sizeThatFits and applyFrame are not thread-safe and attempting to use them on background threads will result in a runtime error.

  4. Optimize HStack performance and avoid double measurements

    main

    QuickLayout may perform up to two measurements to determine child sizes. To ensure high performance:

    • Fixed-size elements (e.g., .frame() or UIImage) are measured only once.
    • Fully flexible elements are not measured directly; they receive a proportion of available space.
    • Avoid double measurements: This occurs if a stack contains two or more partially flexible views (like UILabel) and the proposed size is smaller than their intrinsic content size. Signs of double measurement include unexpected line breaks or text truncation.
    • Solution: Use the .layoutPriority() modifier to specify the measurement order of child elements, which helps prevent multiple measurement passes.
  5. How Frame elements handle proposed size

    main

    A Frame element acts like a 'picture frame': it ignores the size proposed by its parent and instead dictates a specific size to its child.

    When quick_layoutThatFits(_:) is called on a Frame:

    1. The Frame ignores the incoming proposedSize.
    2. It creates a new proposed size based on its own fixed width and height.
    3. The child is then asked to layout using this new fixed size.
    4. The Frame itself adopts the fixed width and height as its own size.
    struct FrameElement: Element {
    
      private let child: Element
      private let width: CGFloat
      private let height: CGFloat
    
      public func quick_layoutThatFits(_ proposedSize: CGSize) -> LayoutNode {
        let frameSize = CGSize(width: width, height: height)
        let childNode = child.quick_layoutThatFits(frameSize)
        return LayoutNode(frameSize, [childNode])
      }
    }
  6. How Padding elements handle proposed size

    main

    Unlike a Frame, the size of a Padding element is dependent on the size of its child.

    When quick_layoutThatFits(_:) is called on a Padding element:

    1. It constructs a new proposed size for the child by subtracting the insets (left, right, top, bottom) from the incoming proposedSize.
    2. The child resolves its intrinsic size within this reduced area.
    3. The Padding element then calculates its own size by taking the child's resolved size and adding the insets back to it.
    struct PaddingElement: Element {
    
      private let child: Element
      private let insets: UIEdgeInsets
    
      public func quick_layoutThatFits(_ proposedSize: CGSize) -> LayoutNode {
        let newProposedSize = CGSize(
          width: proposedSize.width - insets.left - insets.right,
          height: proposedSize.height - insets.top - insets.bottom
        )
    
        let childNode = child.quick_layoutThatFits(newProposedSize)
    
        let paddingSize = CGSize(
          width: childNode.size.width + insets.left + insets.right,
          height: childNode.size.height + insets.top + insets.bottom
        )
        return LayoutNode(size: paddingSize, children: [childNode])
      }
    }
  7. Understand view flexibility categories

    main

    Flexibility defines how a view responds to the size proposed by its parent during the layout process. QuickLayout categorizes views into three primary types of flexibility:

    1. Inflexible Views: These views have a fixed size and disregard the proposed size, always returning their intrinsic dimensions. They are used when a constant size is required (e.g., UISwitch or UIImageView based on image size). Stacks prioritize measuring these views first to optimize performance.
    2. Partially Flexible Views: These views balance content and proposed size. They adjust within bounds based on available space but are still heavily influenced by their content (e.g., UILabel).
    3. Fully Flexible Views: These views have no intrinsic size and adopt the proposed size exactly. They expand or contract to fill the space provided by the parent (e.g., UIScrollView, UITextView, or a plain UIView without a custom sizeThatFits implementation).

    Understanding these categories helps predict how your layout will behave when container sizes change.

  8. Configure alignment in Stacks

    main

    The alignment property in a stack controls how child elements are positioned inside the stack, rather than how the stack itself is positioned within its parent.

    • In a VStack, alignment only affects the horizontal alignment of children.
    • In an HStack, alignment only affects the vertical alignment of children.
  9. Use alignment guides to customize stack alignment

    main

    Alignment guides allow you to override the default alignment behavior of containers like HStack, VStack, or ZStack. While these stacks use standard values (e.g., .center, .leading, .top) by default, you can define custom rules for how children are positioned within the container using the alignmentGuide methods on an Element.

    // Example of applying an alignment guide to an element
    element
      .alignmentGuide(.emojiTitle) { _ in 0 }
  10. How the Element Tree and modifier order work

    main

    QuickLayout translates code into an element tree. Each view in the tree can have modifiers (like .padding or .frame) applied to it.

    Key Principle: Modifier Wrapping Modifiers are applied in the order they appear. Each modifier wraps the previous element with a new element. Because of this wrapping behavior, the order of modifiers directly alters the layout and sizing of the resulting view hierarchy.

    Example: Padding vs Frame Order

    Scenario A: Padding then Frame

    colorView
      .padding(16)
      .frame(width: 100, height: 100)

    In this case, colorView is first wrapped in a padding modifier, and then that entire padded view is wrapped in a frame. The frame constrains the outer container.

    Scenario B: Frame then Padding

    colorView
      .frame(width: 100, height: 100)
      .padding(16)

    Here, the colorView is first constrained to 100x100, and then that 100x100 view is wrapped in padding, which increases the total size of the resulting element tree.

    let colorView = UIView()
    
    var body: Layout {
      colorView
        .padding(16)
        .frame(width: 100, height: 100)
    }