SwiftUI组件封装:仿 Flutter 原生组件 Wrap实现

一、需求来源

在开发Flutter项目时有一个高频使用的组件 Wrap,使用场景特别多。在学习SwiftUI 时发现缺少这个组件,就特别别扭,花点时间实现了一个,遵循极简代码原则。

二、使用示例

swift 复制代码
//
//  WrapDemo.swift
//  SwiftUITemplet
//
//  Created by Bin Shang on 2025/2/27.
//

import SwiftUI

struct WrapDemo: View {

    @StateObject private var router = Router.shared

    var body: some View {
        NavigationStack(path: $router.path) {
            ScrollView(.vertical, showsIndicators: true) {
                VStack(alignment: .leading, spacing: 10, content: {
                    buildWrap(alignment: .leading, itemBgColor: Color.blue);
                    buildWrap(alignment: .center, itemBgColor: Color.green);
                    buildWrap(alignment: .trailing, itemBgColor: Color.red);
                })
            }

            .navigationBar(title: "\(clsName)")//自定义导航封装
  //            .navigationDestination(for: AppPage<AnyView>.self) { page in
  //                page.makeView()
  //            }
        }
    }

    private func buildWrap(
        alignment: HorizontalAlignment = .leading,
        itemBgColor: Color = .blue
    ) -> some View {
        var enumString = ""
        switch alignment {
        case HorizontalAlignment.center:
            enumString = "HorizontalAlignment.center"
        case HorizontalAlignment.trailing:
            enumString = "HorizontalAlignment.trailing"
        default:
            enumString = "HorizontalAlignment.leading"
        }

        return VStack(alignment: alignment, spacing: 10, content: {
            Text("\(enumString)").font(.subheadline)
            Wrap(
                spacing: 10,
                runSpacing: 10,
                alignment: alignment
            ) {
                ForEach(0..<10) { i in
                    Text("选项_\(i)\("z" * i)")
                        .padding(EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 4))
                        .background(itemBgColor)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                        .onTapGesture { p in
                            DDLog("onTapGesture: \(i)")
                        }
                }

           }.padding(10)
            Divider()
        })
    }
}

#Preview {
    WrapDemo()
}

三、源码

1、Wrap 源码
swift 复制代码
//
//  Wrap.swift
//  SwiftUITemplet
//
//  Created by Bin Shang on 2025/2/27.
//

import SwiftUI

//Wrap(spacing: 10,
//      runSpacing: 10,
//      alignment: .leading) {
//    ForEach(0..<10) { i in
//        Text("选项_\(i)\("z" * i)")
//            .padding(EdgeInsets.symmetric(
//                horizontal: 8,
//                vertical: 4))
//            .background(Color.blue)
//            .foregroundColor(.white)
//            .cornerRadius(8)
//            .onTapGesture { p in
//                DDLog("onTapGesture: \(i)")
//            }
//    }
//}
//.padding(10)


/// 折行布局
struct Wrap: Layout {

    /// 水平间距
    var spacing: CGFloat

    /// 竖直间距
    var runSpacing: CGFloat

    /// 对齐方式
    var alignment: HorizontalAlignment

    init(spacing: CGFloat = 10, runSpacing: CGFloat = 10, alignment: HorizontalAlignment = .leading) {
        self.spacing = spacing
        self.runSpacing = runSpacing
        self.alignment = alignment
    }

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {

        // 计算布局所需的总大小
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let maxWidth = proposal.width ?? .infinity
        var totalHeight: CGFloat = 0
        var currentLineWidth: CGFloat = 0
        var currentLineHeight: CGFloat = 0

        for (index, size) in sizes.enumerated() {
            if currentLineWidth + size.width > maxWidth {
                // 换行
                totalHeight += currentLineHeight + runSpacing
                currentLineWidth = size.width
                currentLineHeight = size.height
            } else {
                // 继续当前行
                if index > 0 {
                    currentLineWidth += spacing
                }
                currentLineWidth += size.width
                currentLineHeight = max(currentLineHeight, size.height)
            }
        }

        // 添加最后一行的高度
        totalHeight += currentLineHeight
        return CGSize(width: maxWidth, height: totalHeight)
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {

        let maxWidth = bounds.width

        var y = bounds.minY

        var currentLineHeight: CGFloat = 0

        var currentLineSubviews: [LayoutSubviews.Element] = []

        var currentLineWidth: CGFloat = 0
        
        for subview in subviews {
            let size = subview.sizeThatFits(.unspecified)
            if currentLineWidth + size.width > maxWidth {
                // 换行,放置当前行的子视图
                placeLineSubviews(
                    in: bounds,
                    lineSubviews: currentLineSubviews,
                    lineWidth: currentLineWidth,
                    lineHeight: currentLineHeight,
                    y: y
                )
                y += currentLineHeight + runSpacing
                currentLineSubviews = []
                currentLineWidth = 0
                currentLineHeight = 0
            }

            // 添加子视图到当前行
            currentLineSubviews.append(subview)
            if currentLineSubviews.count > 1 {
                currentLineWidth += spacing
            }
            currentLineWidth += size.width
            currentLineHeight = max(currentLineHeight, size.height)

        }

        // 放置最后一行
        if !currentLineSubviews.isEmpty {
            placeLineSubviews(
                in: bounds,
                lineSubviews: currentLineSubviews,
                lineWidth: currentLineWidth,
                lineHeight: currentLineHeight,
                y: y
            )
        }
    }
    
    private func placeLineSubviews(
        in bounds: CGRect,
        lineSubviews: [LayoutSubviews.Element],
        lineWidth: CGFloat,
        lineHeight: CGFloat,
        y: CGFloat
    ) {
        let maxWidth = bounds.width
        var x = bounds.minX

        // 根据对齐方式调整起始位置
        switch alignment {
        case .leading:
            x = bounds.minX
        case .center:
            x = bounds.minX + (maxWidth - lineWidth) / 2
        case .trailing:
            x = bounds.minX + (maxWidth - lineWidth)
        default:
            x = bounds.minX
        }

        // 放置当前行的子视图
        for subview in lineSubviews {
            let size = subview.sizeThatFits(.unspecified)
            subview.place(at: CGPoint(x: x, y: y), anchor: .topLeading, proposal: .unspecified)
            x += size.width + spacing
        }
    }
}

Layout 协议的核心概念:

Layout 主要涉及 3 个核心方法

  1. sizeThatFits(proposal:subviews:cache:)

    • 计算布局的尺寸 ,返回整个布局的 CGSize
  2. placeSubviews(in:proposal:subviews:cache:)

    • 对子视图进行布局,控制子视图的位置。
  3. makeCache(subviews:)(可选)

    • 缓存计算结果,提高性能。
  4. func updateCache(_ cache:, subviews: )(可选)

    • 更新缓存计算结果,提高性能。

最后、总结

SwiftUI 的 Layout 协议(iOS 16+)提供了一种 结构化、自定义的方式 来管理视图排列。它比 GeometryReader 更清晰、更高效,适用于 复杂布局需求Layout高级用法 ,包括 自适应布局、缓存优化、动画支持、多行流式布局 等。

Layout VS GeometryReader

对比项 Layout 协议 GeometryReader
主要用途 适用于自定义布局 适用于响应父视图尺寸
API 复杂度 结构化,API 规范 容易造成嵌套过深
性能 缓存优化,效率更高 影响父视图布局,可能降低性能
子视图信息 直接获取 Subviews 需要 GeometryProxy 获取
适用场景 复杂布局(如流式布局) 适用于 相对布局

结论

  • Layout 更适合复杂布局 (如 瀑布流环形排列)。
  • GeometryReader 更适合响应父视图尺寸 (如自适应布局)。

github

相关推荐
与火星的孩子对话35 分钟前
Unity进阶课程【六】Android、ios、Pad 终端设备打包局域网IP调试、USB调试、性能检测、控制台打印日志等、C#
android·unity·ios·c#·ip
恋猫de小郭1 天前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
点金石游戏出海1 天前
每周资讯 | Krafton斥资750亿日元收购日本动画公司ADK;《崩坏:星穹铁道》新版本首日登顶iOS畅销榜
游戏·ios·业界资讯·apple·崩坏星穹铁道
旷世奇才李先生1 天前
Swift 安装使用教程
开发语言·ios·swift
90后的晨仔1 天前
Xcode16报错: SDK does not contain 'libarclite' at the path '/Applicati
ios
finger244801 天前
谈一谈iOS线程管理
ios·objective-c
Digitally1 天前
如何将大型视频文件从 iPhone 传输到 PC
ios·iphone
梅名智1 天前
IOS 蓝牙连接
macos·ios·cocoa
美狐美颜sdk2 天前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭2 天前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin