How to use QuickLayout with UIScrollView? #10
-
|
How to use QuickLayout with UIScrollView to achieve the following effect? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
|
QuickLayout doesn’t ship a custom ScrollView type – you still use UIScrollView, and let QuickLayout drive the layout inside the scroll view. To replicate your SwiftUI example, you’d usually do something like this with QuickLayout + UIKit: import UIKit
import QuickLayout
// This view is the "VStack { ViewA; ViewB; ViewC }" part.
@QuickLayout
final class ScrollContentView: UIView {
let viewA = UIView()
let viewB = UIView()
let viewC = UIView()
var body: Layout {
VStack(spacing: 8) {
viewA
viewB
viewC
}
.padding(.horizontal, 16)
.padding(.vertical, 16)
}
}Then embed that inside a UIScrollView: final class MyViewController: UIViewController {
private let scrollView = UIScrollView()
private let contentView = ScrollContentView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.addSubview(contentView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.frame = view.bounds
// Ask QuickLayout how big the content wants to be for a given width
// `sizeThatFits` is implemented for @QuickLayout views
let proposedSize = CGSize(width: scrollView.bounds.width, height: .infinity)
let contentSize = contentView.sizeThatFits(proposedSize)
// Fix the content view to that size, pinned to (0, 0)
contentView.frame = CGRect(origin: .zero, size: contentSize)
// Let UIScrollView know how much it can scroll
scrollView.contentSize = contentView.bounds.size
}
}A few notes: That pattern should give you the same behaviour as your SwiftUI snippet, but in UIKit + QuickLayout. |
Beta Was this translation helpful? Give feedback.
-
|
The ScrollElement wraps the UIScrollView and achieves the effect similar to SwiftUI's ScrollView. For details, please visit: import UIKit
import QuickLayout
// MARK: - ScrollView Container View
/// A scroll view encapsulation with a style similar to SwiftUI ScrollView
/// Reference: https://facebookincubator.github.io/QuickLayout/how-to-use/macro-layout-integration-donts/
@MainActor public func ScrollView(
_ scrollView: QLScrollView,
axis: QuickLayout.Axis = .vertical,
@FastArrayBuilder<Element> children: () -> [Element]
) -> LeafElement & Layout {
ScrollElement(scrollView, axis: axis, children: children())
}
@MainActor
private struct ScrollElement: @MainActor Layout, @MainActor LeafElement {
private let scrollView: QLScrollView
init(
_ scrollView: QLScrollView,
axis: Axis,
children: [Element],
) {
self.scrollView = scrollView
scrollView.axis = axis
scrollView.children = children
}
func quick_layoutThatFits(_ proposedSize: CGSize) -> LayoutNode {
return scrollView.quick_layoutThatFits(proposedSize)
}
func quick_flexibility(for axis: Axis) -> Flexibility {
return scrollView.quick_flexibility(for: axis)
}
func quick_layoutPriority() -> CGFloat {
return scrollView.quick_layoutPriority()
}
func quick_extractViewsIntoArray(_ views: inout [UIView]) {
views.append(scrollView)
}
func backingView() -> UIView? {
scrollView
}
}
import UIKit
import QuickLayout
/// A scrollable view that conforms to QuickLayout's HasBody protocol
///
/// Usage:
/// ```swift
/// let scrollView = QLScrollView()
/// scrollView.axis = .vertical
/// scrollView.children = [label1, label2, label3]
/// ```
open class QLScrollView: UIScrollView, HasBody {
// MARK: - Public Properties
/// The scroll direction (vertical or horizontal)
open var axis: QuickLayout.Axis = .vertical {
didSet {
if axis != oldValue {
configureScrollBehavior()
setNeedsLayout()
}
}
}
/// The child elements to be laid out in the scroll view
open var children: [Element] = [] {
didSet {
setNeedsLayout()
}
}
// MARK: - Initialization
public override init(frame: CGRect) {
super.init(frame: frame)
configureScrollBehavior()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configureScrollBehavior()
}
// MARK: - Configuration
/// Configures the scroll view's behavior based on the current axis.
///
/// This method sets properties like `alwaysBounceVertical`, `alwaysBounceHorizontal`,
/// and scroll indicators to match the desired scroll direction.
private func configureScrollBehavior() {
switch axis {
case .vertical:
alwaysBounceVertical = true
alwaysBounceHorizontal = false
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = true
case .horizontal:
alwaysBounceVertical = false
alwaysBounceHorizontal = true
showsHorizontalScrollIndicator = true
showsVerticalScrollIndicator = false
}
}
// MARK: - HasBody Protocol
@LayoutBuilder
open var body: Layout {
if children.isEmpty {
EmptyLayout()
} else {
axisLayout
}
}
/// Generates the layout for the current axis.
///
/// - Returns: A `VStack` for vertical axis or `HStack` for horizontal axis, containing all children.
@LayoutBuilder
private var axisLayout: Layout {
switch axis {
case .vertical:
VStack(alignment: .leading, spacing: 0) {
ForEach(children)
}
case .horizontal:
HStack(alignment: .top, spacing: 0) {
ForEach(children)
}
}
}
// MARK: - Layout
override open func layoutSubviews() {
super.layoutSubviews()
// Get the alignment based on scroll axis
let alignment = contentAlignment
// Calculate the proposed size for content
let proposedSize = calculateProposedSize()
// Calculate content size using QuickLayout
let contentSize = _QuickLayoutViewImplementation.sizeThatFits(
self,
size: proposedSize
) ?? .zero
// Apply layout with proper alignment
body.applyFrame(
CGRect(origin: .zero, size: contentSize),
alignment: alignment
)
// Update scrollView's contentSize
self.contentSize = contentSize
}
// MARK: - Private Helpers
/// Returns the appropriate alignment based on scroll axis
private var contentAlignment: Alignment {
switch axis {
case .vertical:
return .topLeading
case .horizontal:
return .topLeading
}
}
/// Calculates the proposed size for content based on scroll axis
private func calculateProposedSize() -> CGSize {
let frameSize = frame.size
switch axis {
case .vertical:
// Vertical scrolling: fix width, infinite height
return CGSize(width: frameSize.width, height: .infinity)
case .horizontal:
// Horizontal scrolling: infinite width, fix height
return CGSize(width: .infinity, height: frameSize.height)
}
}
// MARK: - Public Methods
/// Updates the scroll view with new children
/// - Parameter children: The new array of child elements
open func setChildren(_ children: [Element]) {
self.children = children
}
/// Appends children to the existing array
/// - Parameter children: The children to append
open func appendChildren(_ children: [Element]) {
self.children.append(contentsOf: children)
}
/// Removes all children
open func removeAllChildren() {
self.children.removeAll()
}
} |
Beta Was this translation helpful? Give feedback.

QuickLayout doesn’t ship a custom ScrollView type – you still use UIScrollView, and let QuickLayout drive the layout inside the scroll view.
To replicate your SwiftUI example, you’d usually do something like this with QuickLayout + UIKit:
Then embed that inside a UIScrollView: