Swift案例分析《闭包在视图中的传递》
Swift案例分析《闭包在视图中的传递》

Swift案例分析《闭包在视图中的传递》

本文涉及的闭包、变量传递和引用捕获的逻辑。整体过程涉及 ContentView 中的闭包传递 和 EditView 中的闭包使用,以及它们的内存与捕获关系。

闭包示例

在ContentView视图中,当长按地标后,调用Sheet页:

.sheet(item: $selectedPlace) { place in
    EditView(location: place) { newLocation in
        print("Updating \(newLocation)")
        if let index = locations.firstIndex(of: place) {
            locations[index] = newLocation
        }
    }
}

Sheet绑定的selectedPlace是一个@State对象,该对象用于存储选择的地标实例:

@State private var selectedPlace: Location?

Location是一个结构体:

struct Location: Codable, Equatable, Identifiable { }

EditView视图初始化器为一个location变量和一个onSave闭包:

struct EditView: View {
    var location: Location
    var onSave: (Location) -> Void
    @State private var name: String
    @State private var description: String
    
    init(location: Location,onSave: @escaping (Location) -> Void) {
        self.location = location
        self.onSave = onSave
        _name = State(initialValue: location.name)
        _description = State(initialValue: location.description)
    }
    ...
    Button("Save") {
        var newLocation = location
        newLocation.id = UUID()
        newLocation.name = name
        newLocation.description = description

        onSave(newLocation)
        dismiss()
    }
}

闭包被传递给EditView后,存储在onSave属性中,当点击Button按钮时,会将修改后的location属性引用的对象传入闭包,从而完成闭包在视图中的传递和使用。

示例图

代码解析

@State private var selectedPlace: Location?

.sheet(item: $selectedPlace) { place in
    EditView(location: place) { newLocation in
        print("Updating \(newLocation)")
        if let index = locations.firstIndex(of: place) {
            locations[index] = newLocation
        }
    }
}

1、Sheet视图的创建和传递

@State private var selectedPlace: Location?

.sheet(item: $selectedPlace) { place in
    ...
}

@State private var selectedPlace: Location? 是一个可选类型的状态变量,表示当前选中的位置。

当 selectedPlace 被赋值一个非 nil 的值时,sheet 会检测到绑定值的变化,并展示一个 Sheet 视图。

sheet 的 item 参数是一个绑定(Binding),其核心是双向数据流。当 selectedPlace 的值变化时,sheet 视图会随之更新。反之,关闭 Sheet 时,SwiftUI 会将 selectedPlace 重置为 nil

在 Sheet 内部,place 是由 selectedPlace 的当前值拷贝生成的局部变量,用于提供给 Sheet 内容视图的闭包。

2、SwiftUI 的 @State 管理机制

1)@State 的存储管理

@State 标注的变量并非直接存储在 View 本身,而是被 SwiftUI 存储在堆内存的一个外部结构中,用以管理状态的生命周期。

SwiftUI 会自动为 @State 变量生成一个前缀为 _ 的属性(如 _selectedPlace),它是 Binding 的实际实现,包含对状态值的引用。

2)访问机制

selectedPlace 提供了对状态值的访问器,访问其 wrappedValue 会读取或写入存储在堆内存中的实际值。

selectedPlace的读取和写入是通过@State背后的存储机制实现的。

selectedPlace通过_selectedPlace.wrappedValue提供的接口,来实现对状态值的访问,换句话说,selectedPlace是_selectedPlace.wrappedValue的一个快捷方式。

在 sheet 的绑定中,$selectedPlace 是将 selectedPlace 转换为 Binding 类型,它封装了对 wrappedValue 的读写操作,使 SwiftUI 能够感知状态变化。

通过列举selectedPlace、_selectedPlace和$selectedPlace,介绍了selectedPlace的关系,相比之下:

_selectedPlace是 State,它管理状态值并提供基础的存储与访问功能。 wrappedValue更底层,直接操作状态值。

$selectedPlace 是更高层的绑定封装,专为动态绑定场景设计,支持双向数据流。

3、Sheet 的内部逻辑

1)视图生成和更新

当 selectedPlace 的值更新为非 nil 时,Sheet 会调用绑定闭包,生成一个新的视图。

item 是绑定到 selectedPlace 的实际值,当 selectedPlace 改变时,item 也会同步更新,Sheet 内容会随之刷新。

2)状态重置

关闭 Sheet 时,SwiftUI 会自动将 selectedPlace 的值重置为 nil,触发 View 的重新计算。

item 不是直接绑定 $selectedPlace 的地址,而是存储 selectedPlace 的当前值(一个值类型实例),并通过 SwiftUI 的状态驱动机制,自动更新视图。

当 selectedPlace 改变时,item 会接收新的值,因此 item 存储的实际上是 selectedPlace 的快照值,而不是直接的引用地址。

这种机制符合 SwiftUI 的设计哲学:通过状态驱动视图更新,而不是依赖直接的引用或绑定地址。

4、Sheet视图内的闭包

.sheet(item: $selectedPlace) { place in
    EditView(location: place) {
        ...
    }
}

从selectedPlace回到Sheet视图本身,{ place in }是一个闭包,

sheet(item: Binding<Item?>, content: @escaping (Item) -> Content)   // 省略写法

SwiftUI 会将这个闭包保存在堆内存中,指针(content)存储闭包的地址。

这样做的目的是:即使在不同的状态更新中,可以动态调用该闭包生成视图内容。

闭包执行时,以下内容发生

栈内存中,place 变量被创建,用于持有 item(即 selectedPlace 解包后的值)的当前快照。

捕获环境:locations 数组是闭包外部变量,它会被捕获到闭包的捕获环境中,但仅捕获引用(详解见后面的“数组的引用问题”)。

content 闭包的返回值是一个新的 EditView,该视图在堆内存中创建并存储,成为 Sheet 的一部分。

EditView 的 location 参数接收的是 place 的值。

数组的引用问题

我们在捕获locations会注意到,捕获的是引用。而locations是一个数组,数组的元素类型是Location结构体,所以数组和元素都是值类型,按理说捕获环境获取的是locations的值,发生值拷贝,而不是引用

但 Swift 的数组有一个 优化机制,叫做“写时复制(Copy-on-Write, COW)”。它在实际操作中表现为引用语义的某些特性。

写时复制(Copy-on-Write, COW)

虽然数组是值类型,但 Swift 优化了性能,避免在每次传递数组时立即拷贝整个数组。具体行为是:

传递数组时:不会立即拷贝,而是让多个变量共享同一个底层存储(堆内存中的数组内容)。

修改数组时:如果某个共享数组被修改,Swift 会自动创建该数组的独立副本,然后对副本进行操作(此时才发生真正的拷贝)。

这种机制表现为:在读取或传递数组时,行为类似引用语义;而在修改数组时,仍然符合值类型的特性。

当捕获 locations 时,Swift 实际捕获的是 locations 本身的引用(不是底层存储的直接引用),因为 Swift 的捕获规则中:

1)值类型变量直接捕获其值(值语义)。

2)可变值类型(如数组)如果发生修改,会遵循写时复制规则,影响捕获到的变量。

捕获时的行为

捕获 locations 的数组变量(例如在闭包中)时:

捕获的是 locations 自身,而不是底层存储的直接指针。

如果闭包内修改了 locations,Swift 会执行写时复制,生成一个新数组并修改。

数组引用结论

locations 本质是值类型,但因为:

1)它被 @State 修饰,SwiftUI 会通过状态管理封装它。

2)Swift 的数组采用写时复制机制,在某些操作下表现出类似引用语义的特性。

所以描述 locations 捕获为“引用”是合理的,但其核心还是值类型。

5、EditView视图的传递

struct EditView: View {
    var location: Location
    var onSave: (Location) -> Void
    init(location: Location,onSave: @escaping (Location) -> Void) {
        self.location = location
        self.onSave = onSave
    }
    ...
}

EditView 的 {newLocation in}闭包被传递给EditView视图的onSave属性,这个闭包被存储在EditView的视图区域内,引用了外部变量(如locations),因此形成了捕获环境。

捕获环境的共享

1、Sheet 和 EditView 间的捕获环境

onSave 捕获了外部的 locations。

虽然 Sheet 和 EditView 是不同的视图,但 locations 作为闭包的捕获变量,实际上引用的是同一个对象。

捕获环境是共享的,因此对 locations 的修改能被所有持有该捕获环境的地方感知。

2、捕获行为

locations 是数组(值类型),但因为它是 @State 属性的存储,SwiftUI 的状态管理会将其包装在引用类型中。

因此,闭包捕获的是 locations 的引用(底层是状态存储对象),保证能追踪数组的修改。

当在EditView视图中点击Button(“Save”)按钮时:

Button("Save") {
    var newLocation = location
    newLocation.id = UUID()
    newLocation.name = name
    newLocation.description = description

    onSave(newLocation)
    dismiss()
}

1、栈上的 newLocation 变量

点击按钮时,Swift 会在栈上创建一个 newLocation 变量。

newLocation 是对 location 的拷贝。

2、修改 newLocation

在闭包中对 newLocation 的修改不会影响 location,因为它们是不同的实例。

3、调用 onSave 闭包

onSave(newLocation)

调用时,newLocation 的值被拷贝传递到 onSave。

在 onSave 内部,闭包的 newLocation 是一个独立的栈变量,但值与按钮点击中生成的 newLocation 相同。

6、onSave 闭包内的执行逻辑

{ newLocation in
    print("Updating \(newLocation)")
    if let index = locations.firstIndex(of: place) {
        locations[index] = newLocation
    }
}

1、修改 locations

闭包内的 locations 是捕获的外部变量,指向 SwiftUI 状态管理的包装对象。

使用 locations.firstIndex(of: place) 查找索引,修改数组的对应元素。

2、状态更新

栈内存创建index变量用于保存返回的索引。

locations[index] = newLocation 会触发 SwiftUI 重新计算绑定到 locations 的视图,更新界面。

以上就是全部的知识内容,从内存、引用、闭包等多个角度分析,闭包是如何传递到各视图,以及如何调用的,知识量很多,这个代码从一步一步理解、分析到完整的写完这个教程,差不多也花费了我近4天的时间,希望看完该教材后,对更多的人有所帮助。

扩展知识

locations数组的修改

结尾数组的引用问题,在这里进一步分析:

locations[index] = newLocation
1、获取 locations 的引用

通过捕获环境引用 locations。

捕获环境存储了 locations 的引用,指向 SwiftUI 的 StateStorage 对象,实际数组存储在堆内存中。

2、检查写时复制条件

Swift 检查数组是否有其他引用共享其底层存储。

如果没有共享(例如只有一个闭包捕获了 locations),直接修改底层存储。

如果存在共享引用(比如其他视图或线程正在访问相同的 locations),Swift 执行写时复制。

3、执行修改

1、如果没有触发写时复制

直接在数组的堆内存中,将第 index 个元素替换为 newLocation。

2、如果触发了写时复制

创建一个新数组,将原始数组的数据复制到新数组。

在新数组中,替换第 index 个元素。

更新捕获环境中的 locations 引用,让它指向新数组。

locations修改没有触发写时复制,因此属于第一种。

是否直接修改数组索引指向的实际数组?

捕获环境中的 locations 并不是直接捕获数组本身,而是捕获 @State 的包装器对象。因此:

修改行为:实际操作的是包装器对象管理的底层数组存储。

写时复制:在包装器对象中触发,包装器对象会替换底层数组的存储。

所以从开发者的角度来看,修改是直接对数组的操作;但底层实现会根据写时复制的规则决定是否生成新数组。

locations为什么没有触发写时复制?

content闭包和onSave闭包都捕获了locations数组,存在多处引用,但还需要考虑 SwiftUI 的 @State 特性。

SwiftUI 的 @State 对引用语义的影响

在 SwiftUI 中,@State 属性包装器将值类型(如数组)包装成一种引用语义对象,通常是某种 StateStorage 类,实际数组存储在堆内存中。这样设计的目的是追踪状态变化,以便触发视图更新。

1、locations 的引用

外层捕获环境中的 locations 和 onSave 捕获的 locations 引用的实际上是同一个 StateStorage 对象(不是直接引用数组)。

由于这两个引用指向同一个包装器对象,在默认情况下,并不会触发写时复制。

2、数组修改行为

当执行 locations[index] = newLocation 时,操作的实际上是 StateStorage 管理的数组。

如果 StateStorage 的内部实现确保没有其他视图或状态系统共享底层数组存储,那么修改是直接在原数组上进行的,不会触发写时复制。

写时复制的触发条件

在 Swift 的写时复制机制下,触发 COW 的条件是:

1、是否有多个独立引用共享同一底层数组存储。

2、是否需要保护值类型的独立性(即保证修改后不同引用不受影响)。

但是,SwiftUI 的 @State 属性包装器破坏了值类型的独立性,因为它将数组包装成了引用语义。因此:

locations 在不同捕获环境中的引用本质上是同一个 StateStorage 对象。

修改 locations 并不会触发写时复制,而是直接在原数组的存储上进行修改。

locations[index] = newLocation 的执行过程

假设 locations 是 @State 修饰的数组:

1、查找索引

firstIndex(of:) 查找到目标元素的索引 index。

index 是栈内存中的临时变量。

2、修改数组元素

调用 locations[index] = newLocation。

如果只有一个引用持有底层数组存储,直接在原存储上修改。

如果存在其他共享引用,触发写时复制(但在 @State 管理下通常不会发生)。

为什么不会触发写时复制?

SwiftUI 的 @State 属性包装器和捕获环境的引用机制导致:

捕获的是引用语义包装器(StateStorage 对象),而不是直接捕获数组本身。

共享的是包装器,而不是底层数组存储,因此修改时不会触发 COW。

只有在不使用 @State,直接捕获普通数组的情况下,修改才可能触发写时复制。

关于写时复制的总结为

1、闭包捕获的是 @State 包装器的引用,而非直接捕获值类型数组。

2、@State 包装器内部用引用语义管理值类型,因此直接修改通常不会触发写时复制。

3、只有当底层值被多个独立引用共享时,写时复制才会被触发。

为什么需要写时复制?

写时复制是为了确保值类型的语义完整性。在以下场景中会显得尤为重要:

1、多个视图共享同一个 locations

如果不触发写时复制,修改会导致多个视图同时看到变化,违背了值类型的独立性原则。

2、线程安全

写时复制避免了多个线程同时修改同一个数据的风险。

这种机制在保证值类型语义的同时,结合引用语义提升了性能和灵活性,是 Swift 语言的重要特性之一。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注