一、SwiftUI的声明式UI本质

SwiftUI的本质是"UI即状态的函数"——给定状态,产出视图描述(View Tree)。这个描述是值类型(Value Type),每次状态变更都会生成一棵新的视图树。框架通过比对新旧两棵树(Diffing),计算出最小更新集(Minimal Update Set),只渲染实际变化的部分。

1.1 View Tree 的 Diffing 机制

// SwiftUI的视图构建是声明式的
struct ContentView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")  // count 变化时,这行会更新
            Button("Increment") {
                count += 1          // 状态变更 → 新View Tree → Diff → 局部更新
            }
        }
    }
}

// SwiftUI内部执行流程(编译时生成):
// 1. combineLatest(@Published count, ...)  // 监听count变化
// 2. body 被调用,生成新的 ViewDescription(结构化描述,非真实UI)
// 3. ViewDescription 与上一次的进行对比(Tree Diff)
// 4. 计算出 delta(变化的部分)
// 5. 将 delta 提交给 Renderer(Metal-backed)
// 6. Metal 绘制变更的区域(增量绘制,不是全量重绘)

1.2 Equatable 的关键作用

SwiftUI通过判断 View 的 Equatable 实现来决定是否需要更新。如果两个状态相等,就跳过重新渲染:

// 错误:没有实现Equatable,每次都重新渲染
struct UserCard: View {
    let user: User  // User可能很大,但diffing只看引用

    var body: some View {
        VStack {
            Text(user.name)
            Text(user.email)
        }
        // 问题:如果user.name没变但user.email变了,整个Card还是会重建
    }
}

// 正确:精确控制diffing粒度
struct UserCard: View {
    let user: User

    var body: some View {
        VStack {
            Text(user.name)       // 仅当name实际变化时才更新
                .id(user.name)   // Text视图用name作为identity
            Text(user.email)     // 仅当email变化时才更新
                .id(user.email)
        }
    }
}

// 更进一步:视图级别Equatable
struct UserCard: View, Equatable {
    let user: User

    static func == (lhs: UserCard, rhs: UserCard) -> Bool {
        lhs.user.name == rhs.user.name &&   // 精确对比
        lhs.user.email == rhs.user.email &&
        lhs.user.avatarURL == rhs.user.avatarURL
    }
}

二、视图身份与识别机制

2.1 View Identity 冲突

SwiftUI 使用显式身份(通过 .id())和隐式身份(View Tree 位置)来追踪视图。当两者冲突时,会导致视图被意外销毁重建:

// 问题案例:ForEach使用index作为身份(index变化导致重建)
struct BadExample: View {
    @State private var items = ["A", "B", "C"]

    var body: some View {
        VStack {
            ForEach(items.indices, id: \.self) { index in  // index作为ID
                Text(items[index])  // 插入新元素时,index重排,原有视图被重建
            }
            Button("Add Item") {
                items.insert("X", at: 0)  // 插入到开头,index重排
            }
        }
    }
}

// 解决方案:使用稳定唯一ID
struct GoodExample: View {
    @State private var items = [
        Item(id: UUID(), name: "A"),
        Item(id: UUID(), name: "B"),
    ]

    var body: some View {
        VStack {
            ForEach(items) { item in  // Item 需实现 Identifiable
                Text(item.name)       // name变化时仅重建Text,不是整个列表
            }
        }
    }
}

// 删除中间项时:只需要移动B和C的position,不是销毁重建
// 这是SwiftUI视图复用的关键原理

三、渲染性能优化实战

3.1 惰性加载(Lazy Loading)

LazyVStack/LazyHStack 只渲染屏幕可见范围内的视图,是长列表的必备优化:

// 基础LazyVStack
struct ItemList: View {
    let items: [Item]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(items) { item in
                    ItemRow(item: item)
                    // SwiftUI自动计算可见范围
                    // 只实例化可见项的视图对象
                    // 滚动时,移出屏幕的视图被回收(view pool)
                }
            }
        }
    }
}

// 网格布局:LazyVGrid
struct ItemGrid: View {
    let items: [Item]
    private let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(items) { item in
                    ItemCell(item: item)
                }
            }
        }
    }
}

3.2 减少视图重建的优先级控制

// @StateObject vs @ObservedObject vs @State
// @State: 值拥有,适用于简单状态
// @StateObject: 对象拥有,适用于引用类型(class),对象创建时初始化一次
// @ObservedObject: 不拥有,适用于父视图传递进来的对象

// 常见错误:在body中创建ObservableObject(每次渲染都新建)
struct BadView: View {
    // 错误:每次body重新计算都创建新的ViewModel
    var body: some View {
        ListView(viewModel: ListViewModel()) // ❌ ViewModel是class,重新创建浪费
    }
}

// 正确:用@StateObject拥有ViewModel
struct GoodView: View {
    @StateObject private var viewModel = ListViewModel() // ✅ 初始化一次
    var body: some View {
        ListView(viewModel: viewModel)
    }
}

// @environmentObject: 用于深度传递共享的ViewModel
// 避免中间层每个View都声明@ObservedObject

3.3 避免约束条件下的重复计算

SwiftUI的视图体(body)是"计算属性",每次父视图状态变化时,body都会被重新调用。昂贵的计算放在 body 之外:

struct ExpensiveView: View {
    let items: [ExpensiveItem]
    @State private var searchText = ""

    // ✅ computed属性在body外缓存,不重复计算
    private var filteredItems: [ExpensiveItem] {
        // 这个计算只在这里调用,items变化时重新计算
        items.filter { item in
            // 假设这个过滤很昂贵
            item.name.localizedCaseInsensitiveContains(searchText)
        }
    }

    // ✅ 计算属性缓存(items或searchText变化时才重新计算)
    private var sortedItems: [ExpensiveItem] {
        filteredItems.sorted { $0.name < $1.name }
    }

    var body: some View {
        // ❌ 不要在body内直接写计算
        // ForEach(items.filter { ... }.sorted { ... }) ← filter和sort每次都执行

        // ✅ 使用缓存的计算属性
        ForEach(sortedItems) { item in
            ItemRow(item: item)
        }
    }
}

四、列表滚动性能优化

4.1 Prefetching 预加载

// 使用LazyVStack的onAppear优化(滚动加载更多)
struct InfiniteListView: View {
    @StateObject private var viewModel = PaginatedListViewModel()

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(viewModel.items) { item in
                    ItemRow(item: item)
                        .onAppear {
                            // 当前行出现时检查是否需要加载更多
                            if item == viewModel.items.last {
                                Task {
                                    await viewModel.loadNextPage()
                                }
                            }
                        }
                }

                // 加载指示器
                if viewModel.isLoading {
                    ProgressView()
                        .padding()
                }
            }
        }
    }
}

// 大型图片列表的内存优化
struct OptimizedImageRow: View {
    let imageURL: URL?

    var body: some View {
        AsyncImage(url: imageURL) { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(height: 200)
                    .clipped()  // 裁剪超出区域
            case .failure:
                Image(systemName: "photo")
                    .foregroundColor(.gray)
            @unknown default:
                EmptyView()
            }
        }
    }
}

五、内存与视图回收

5.1 PreferenceKey 实现精确布局

PreferenceKey 允许子视图向父视图传递测量数据,用于复杂的手动布局:

// 场景:多列布局中,每列高度不同,需要统一对齐
struct ColumnHeightPreference: PreferenceKey {
    static var defaultValue: [Int: CGFloat] = [:]

    static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
        value.merge(nextValue(), uniquingKeysWith: max) // 保留最大高度
    }
}

struct SelfSizingColumn: View {
    let index: Int
    let content: String

    var body: some View {
        Text(content)
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(
                GeometryReader { geo in
                    Color.clear.preference(
                        key: ColumnHeightPreference.self,
                        value: [index: geo.size.height]
                    )
                }
            )
    }
}

// 父视图读取所有列高度
struct MultiColumnView: View {
    @State private var heights: [Int: CGFloat] = [:]

    var body: some View {
        HStack(alignment: .top, spacing: 16) {
            ForEach(0..<3, id: \.self) { index in
                SelfSizingColumn(index: index, content: "Column \(index)")
            }
        }
        .onPreferenceChange(ColumnHeightPreference.self) { newHeights in
            heights = newHeights  // 收集所有列的高度
        }
    }
}

SwiftUI性能优化清单

  • 所有@State/@Published对象实现Equatable,精确触发更新
  • ForEach使用稳定唯一ID,不用index作为身份
  • 大型列表必须使用LazyVStack/LazyVGrid
  • 避免在body内直接创建ObservableObject实例
  • 大图片使用AsyncImage + .resizable(),不要在body中decode
  • @StateObject在父视图创建,@ObservedObject接收父视图传递的对象