You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

遵循Layout协议自定义流式布局失败:totalWidth值异常致单列显示

问题分析与修复

核心问题

你的流式布局始终单列显示,根源在于sizeThatFits方法的返回值错误:你返回了所有行中的最大宽度(也就是第一个子视图的宽度),这会让SwiftUI认为你的布局只需要这么宽的空间,后续placeSubviews拿到的bounds.width自然也等于这个值,导致每次添加子视图时都触发换行逻辑。

修复步骤

  1. 修改sizeThatFits方法,优先使用父容器提供的可用宽度作为布局的总宽度,而非子视图的最大宽度。
  2. 重构getRows方法的行坐标计算逻辑,基于已有的行总高度计算下一行位置,避免依赖单个子视图坐标导致的间距错误。

完整修复代码

struct FlowLayoutView: View {    

    let tags = ["WWDC22", "WWDC23", "SwiftUI", "Swift", "Apple", "iPhone", "iOS", "iPad", "macOS", "M2", "tech", "MacBook", "developer", "news", "coding", "Xcode", "UIKit", "AppKit", "Love", "Hello world!"]
    
    var body: some View {
        ScrollView {
            FlowLayoutDemo {
                ForEach(tags, id: \.self) { tag in
                    Button(tag) {}.buttonStyle(.bordered)
                }
            }
            .padding()
            .background(.yellow)
        }
    }
}

struct FlowLayoutDemo: Layout {

    var hSpacing: CGFloat = 10
    var vSpacing: CGFloat = 10
    
    struct Row {
        var viewRects: [CGRect] = []

        // 行的实际宽度:最后一个视图的maxX
        var width: CGFloat { viewRects.last?.maxX ?? 0 }
        // 行的高度:所有视图中的最大高度
        var height: CGFloat { viewRects.map(\.height).max() ?? 0 }
    }
    
    private func getRows(subviews: Subviews, totalWidth: CGFloat?) -> [Row] {
        guard let totalWidth, !subviews.isEmpty else { return [] }

        var rows: [Row] = []
        // 跟踪当前行的已用宽度、最大高度和视图坐标
        var currentRowWidth: CGFloat = 0
        var currentRowHeight: CGFloat = 0
        var currentRowRects: [CGRect] = []
        // 记录所有行的总高度,用于计算下一行的y坐标
        var totalRowsHeight: CGFloat = 0

        subviews.forEach { view in
            let size = view.sizeThatFits(.init(width: nil, height: nil))
            
            // 计算当前视图需要的宽度(包含左侧间距,首视图无间距)
            let requiredWidth = size.width + (currentRowWidth == 0 ? 0 : hSpacing)
            if currentRowWidth + requiredWidth > totalWidth {
                // 当前行无法容纳,保存当前行并开启新行
                if !currentRowRects.isEmpty {
                    rows.append(Row(viewRects: currentRowRects))
                    totalRowsHeight += currentRowHeight + vSpacing
                }
                currentRowWidth = size.width
                currentRowHeight = size.height
                currentRowRects = [CGRect(x: 0, y: totalRowsHeight, size: size)]
            } else {
                // 添加到当前行
                let x = currentRowWidth == 0 ? 0 : currentRowWidth + hSpacing
                let y = totalRowsHeight
                let rect = CGRect(x: x, y: y, size: size)
                currentRowRects.append(rect)
                currentRowWidth += requiredWidth
                currentRowHeight = max(currentRowHeight, size.height)
            }
        }
        
        // 添加最后一行
        if !currentRowRects.isEmpty {
            rows.append(Row(viewRects: currentRowRects))
        }

        return rows
    }
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let totalWidth = proposal.width ?? .infinity
        let rows = getRows(subviews: subviews, totalWidth: totalWidth)
        
        // 宽度优先使用父容器提供的可用宽度,高度为所有行的总高度
        let width = proposal.width ?? rows.map(\.width).max() ?? 0
        let height = rows.last?.viewRects.map(\.maxY).max() ?? 0
        return CGSize(width: width, height: height)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let rows = getRows(subviews: subviews, totalWidth: bounds.width)
        var index = 0
        rows.forEach { row in
            row.viewRects.forEach { rect in
                subviews[index].place(
                    at: CGPoint(x: rect.minX + bounds.minX, y: rect.minY + bounds.minY),
                    proposal: ProposedViewSize(rect.size)
                )
                index += 1
            }
        }
    }
}

关键修复点说明

  • sizeThatFits方法:不再返回子视图的最大宽度,而是优先采用父容器传入的proposal.width,确保布局占满可用空间,让placeSubviews能获取正确的总宽度。
  • getRows方法:用状态变量跟踪当前行的布局信息,替代之前依赖单个子视图坐标的逻辑,行间距和换行判断更准确。
  • 子视图尺寸计算:调用sizeThatFits时传入nil宽度,让子视图返回自身固有尺寸,避免强制限制宽度导致的尺寸计算错误。

内容的提问来源于stack exchange,提问作者Chow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 14:19:49