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

SwiftUI Layout协议流式布局异常问题求助

SwiftUI 自定义流式布局(FlowLayout)修复方案

问题根源

  1. 子视图尺寸计算时使用.unspecified,导致长文本、宽按钮无法自动换行,直接溢出父视图
  2. 未处理单个子视图宽度超过可用空间的场景,宽元素直接越界
  3. 换行判断依赖proposal.width,而非父视图实际可用的bounds.width,造成空间利用不充分

修复后的完整代码

import SwiftUI

struct MessageAudioRecorder: View {
    var body: some View {
        FlowLayout {
            Text("Hello Hello Hello Hello Hello Hello Hello")
            Button {
                
            } label: {
                Text("111111111111111111").foregroundStyle(.blue)
            }
            Text("Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello")
            Text("111111111111111111").foregroundStyle(.orange)
        }
        .padding() // 可选:给布局加内边距,避免贴边
    }
}

#Preview {
    MessageAudioRecorder()
}

struct FlowLayout: Layout {
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        // 计算每个子视图在当前可用宽度下的实际尺寸(限制最大宽度为bounds.width)
        let availableWidth = bounds.width
        let sizes = subviews.map { subview in
            subview.sizeThatFits(ProposedViewSize(width: availableWidth, height: .infinity))
        }
        
        var lineX = bounds.minX
        var lineY = bounds.minY
        var lineHeight: CGFloat = 0
        
        for index in subviews.indices {
            let currentSize = sizes[index]
            // 检查当前子视图加入后是否超出可用宽度,单个子视图超宽则直接换行
            if lineX + currentSize.width > bounds.maxX, lineX != bounds.minX {
                lineY += lineHeight
                lineHeight = 0
                lineX = bounds.minX
            }
            
            // 放置子视图
            subviews[index].place(
                at: CGPoint(x: lineX + currentSize.width / 2, y: lineY + currentSize.height / 2),
                anchor: .center,
                proposal: ProposedViewSize(currentSize)
            )
            
            lineHeight = max(lineHeight, currentSize.height)
            lineX += currentSize.width
        }
    }
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let availableWidth = proposal.width ?? .infinity
        // 计算每个子视图在限制宽度下的尺寸
        let sizes = subviews.map { subview in
            subview.sizeThatFits(ProposedViewSize(width: availableWidth, height: .infinity))
        }
        
        var totalHeight: CGFloat = 0
        var lineWidth: CGFloat = 0
        var lineHeight: CGFloat = 0
        
        for size in sizes {
            // 处理单个子视图超宽的情况
            let adjustedWidth = min(size.width, availableWidth)
            if lineWidth + adjustedWidth > availableWidth {
                totalHeight += lineHeight
                lineWidth = adjustedWidth
                lineHeight = size.height
            } else {
                lineWidth += adjustedWidth
                lineHeight = max(lineHeight, size.height)
            }
        }
        totalHeight += lineHeight
        
        // 返回的宽度不超过父视图提议的宽度,避免溢出
        return CGSize(width: min(lineWidth, availableWidth), height: totalHeight)
    }
}

关键修改说明

  • 子视图尺寸计算:给每个子视图传递带最大宽度的ProposedViewSize,强制长文本/宽按钮自动换行适应可用空间
  • 超宽元素处理:单个子视图宽度超过可用空间时,直接调整为可用宽度,避免溢出
  • 换行判断:使用bounds.maxX(父视图实际右边界)判断是否换行,确保布局严格在可用空间内
  • 尺寸适配:sizeThatFits返回的宽度限制在父视图提议的宽度内,避免布局整体超出父视图范围

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:12:11