遵循Layout协议自定义流式布局失败:totalWidth值异常致单列显示
问题分析与修复
核心问题
你的流式布局始终单列显示,根源在于sizeThatFits方法的返回值错误:你返回了所有行中的最大宽度(也就是第一个子视图的宽度),这会让SwiftUI认为你的布局只需要这么宽的空间,后续placeSubviews拿到的bounds.width自然也等于这个值,导致每次添加子视图时都触发换行逻辑。
修复步骤
- 修改
sizeThatFits方法,优先使用父容器提供的可用宽度作为布局的总宽度,而非子视图的最大宽度。 - 重构
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
相关产品推荐
相关产品推荐

