QuickLayout
repository·main·Indexed 43 days ago
https://github.com/facebook/quicklayoutA 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.
What's inside QuickLayout
- QuickLayout is a declarative layout library designed for UIKit-first iOS applications. It provides a simple abstraction layer to express layouts declaratively without replacing UIKit itself. The library focuses on a minimal API surface with concepts familiar to iOS engineers to solve common layout problems efficiently.
Core Features of QuickLayout
mainQuickLayout 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, andFlowthat do not add extra views to the hierarchy. - Easy Integration: The
@QuickLayoutmacro 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).
What is LazyView and how does it work?
mainA
LazyViewis 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.LazyViewbehaves similarly to lazy variables in Swift, but includes anifLoadedgetter. 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 }Understand QuickLayout thread safety constraints
mainThe core QuickLayout layout algorithm (including
VStack,HStack,frame, andpadding) 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
UIViewinstances, you must perform layout operations on the main thread. This is becauseUIViewmethods likesizeThatFitsandapplyFrameare not thread-safe and attempting to use them on background threads will result in a runtime error.Optimize HStack performance and avoid double measurements
mainQuickLayout may perform up to two measurements to determine child sizes. To ensure high performance:
- Fixed-size elements (e.g.,
.frame()orUIImage) 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.
- Fixed-size elements (e.g.,
How Frame elements handle proposed size
mainA
Frameelement 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 aFrame:- The
Frameignores the incomingproposedSize. - It creates a new proposed size based on its own fixed
widthandheight. - The child is then asked to layout using this new fixed size.
- The
Frameitself adopts the fixedwidthandheightas 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]) } }- The
How Padding elements handle proposed size
mainUnlike a
Frame, the size of aPaddingelement is dependent on the size of its child.When
quick_layoutThatFits(_:)is called on aPaddingelement:- It constructs a new proposed size for the child by subtracting the
insets(left, right, top, bottom) from the incomingproposedSize. - The child resolves its intrinsic size within this reduced area.
- The
Paddingelement then calculates its own size by taking the child's resolved size and adding theinsetsback 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]) } }- It constructs a new proposed size for the child by subtracting the
ZStack Performance Considerations
mainWhen usingZStack, note that the layout engine measures each child element exactly once, making it an efficient way to layer views.Understand view flexibility categories
mainFlexibility 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:
- 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.,
UISwitchorUIImageViewbased on image size). Stacks prioritize measuring these views first to optimize performance. - 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). - 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 plainUIViewwithout a customsizeThatFitsimplementation).
Understanding these categories helps predict how your layout will behave when container sizes change.
- 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.,
Configure alignment in Stacks
mainThe
alignmentproperty 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,alignmentonly affects the horizontal alignment of children. - In an
HStack,alignmentonly affects the vertical alignment of children.
- In a
Use alignment guides to customize stack alignment
mainAlignment guides allow you to override the default alignment behavior of containers like
HStack,VStack, orZStack. 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 thealignmentGuidemethods on anElement.// Example of applying an alignment guide to an element element .alignmentGuide(.emojiTitle) { _ in 0 }How the Element Tree and modifier order work
mainQuickLayout translates code into an
element tree. Each view in the tree can have modifiers (like.paddingor.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,
colorViewis 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
colorViewis 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) }