在这篇文章中,我们将探讨几个在 SwiftUI 开发中常常运用且至关重要的特点包装器。本文旨在供给对这些特点包装器的主要功用和运用注意事项的概述,而非详尽的运用指南。

本文应几位朋友之邀而写,旨在帮助已经了解通用编程但对 SwiftUI 相对生疏的开发者,快速了解这些特点包装器的核心作用和适用场景。

原文发表在我的博客fatbobman.com 。 因为技术文章需求不断的迭代,当时耗费了不少的精力在不同的渠道之间来维持文章的更新。故从 2024 年起,新的文章将只发布在我的博客上

@State

@State 是 SwiftUI 中最常用的特点包装器之一,主要用于在视图内部办理私有数据。它特别适合存储值类型数据,如字符串、整数、枚举或结构体实例。

  • @State 用于办理视图的私有状况。
  • 它主要用于存储值类型数据(与视图的生命周期共同)。

典型运用场景

  • 当需求因视图内的数据改变而触发视图更新时,@State 是理想的挑选。
  • 它常用于简单的 UI 组件状况办理,如开关状况、文本输入等。
  • 假如数据不需求杂乱的跨视图同享,运用 @State 能够简化状况办理。

注意事项

  • 尽量仅在视图的内部运用 @State,即便未显式标记为 private,也应当将其视为视图的私有特点。

  • @State 为包装数据同时供给了双向数据绑定管道,能够经过 $ 前缀来拜访。

  • @State 不适合用于存储大量数据或杂乱数据模型,这种状况下更适合运用 @StateObject 或其他状况办理方案。

  • 特点包装器本质上是一个结构体。运用 @ 前缀时,它用于包装其他数据;而不带 @ 时,表明其本身类型。更多细节参阅 John SundellAntoine van der Lee,或阅览 @State 研究

  • 在构造办法中赋值时,需经过 _ 下划线拜访 @State 的原始值并进行赋值。

@State var name: String
init(text: String) {
    // 给下划线版本赋值,需求用 State 类型本身进行包装
    _name = State(wrappedValue: text)
}
  • @State 变量在视图的构造函数中只能赋值一次,后续的调整需求在视图的 body 内进行。详见 防止 SwiftUI 视图的重复计算

  • 假如不需求在当时视图或在子视图中(经过 @Binding )修正值,无需运用 @State

  • 在某些状况下, @State 也被用来存储非值类型数据,比方引证类型以保证其仅有性和生命周期。

@State var textField: UITextField?
TextField("", text: $text)
    .introspect(.textField, on: .iOS(.v17)) {
        // 持有 UITextField 实例
        self.textField = $0
    }
  • @State 在 Observation 结构中用于保证 @Observable 实例的生命周期不短于视图本身。详细信息见 深度解读 Observation
  • @State 是线程的安全,能够在非主线程中进行修正。
@State var text: String = ""
Button("Change") {
    // 无需切换回主线程
    Task.detached {
        text = "hi"
    }
}

@Binding

@Binding 是 SwiftUI 中用于完成双向数据绑定的特点包装器。它创立了值(如 Bool)与显现及修正这些值的 UI 元素之间的双向连接。

  • @Binding 不直接持有数据,而是供给了对其他数据源的读写拜访的包装。
  • 它答应 UI 元素直接修正数据,并反映这些数据的改变。

典型运用场景

  • @Binding 主要用于与支撑双向数据绑定的 UI 组件,如和 TextFieldStepperSheetSlider 等配合运用。
  • 它适用于需求在子视图中直接修正父视图中的数据状况。

注意事项

  • 应当慎重运用 @Binding,当子视图只需呼应数据改变而无需修正时,无需运用 @Binding

  • 在杂乱的视图层级中,逐级传递 @Binding 或许导致数据流难以追寻,此时应考虑运用其他状况办理办法。

  • 保证 @Binding 的数据源是可信的,过错的数据源或许导致数据不共同或运用溃散。因为 @Binding 只是一个管道,它并不保证对应的数据源在调用时必定存在。

  • 开发者能够经过供给 getset 的办法来自界说 Binding。

let binding = Binding<String>(
    get: { text },
    // 限制字符串的长度
    set: { text = String($0.prefix(10)) }
)
  • 经过为 Binding 类型创立扩展,能够极大地进步开发的功率和灵活性。相关内容请阅览:SwiftUI Binding Extensions
// 将一个 Binding<V?> 转换为 Binding<Bool>
extension Binding {
    static func isPresented<V>(_ value: Binding<V?>) -> Binding<Bool> {
        Binding<Bool>(
            get: { value.wrappedValue != nil },
            set: {
                if !$0 { value.wrappedValue = nil }
            }
        )
    }
}
  • 在 Observation 结构中,能够运用 @Bindable@Observable 实例创立对应的 Binding 接口,详细信息见 深度解读 Observation。。

  • 在声明构造参数时,需求明确指定 Binding 的包装值类型(get 办法的返回值类型),如 Binding<String>

  • @Binding 并不是独立的数据源。实际上,它只是对已存在数据的引证。只要能够引发视图更新的值被 get 办法读取时,才会触发视图更新( 比方 @State、@StateObject ),这点关于自界说 Binding 尤为重要。

struct Test: View {
    let a = A()
    var body: some View {
        let binding = Binding<String>(
            get: { a.name },
            set: { a.name = $0 }
        )
        // 虽然 A 契合 ObservableObject 协议,但是因为没有运用 StateObject 与视图相关,因此为其特点创立的 Binding 也同样不会引发视图更新
        Text(binding.wrappedValue)
        TextField("input:", text: binding)
    }
    class A: ObservableObject {
        @Published var name: String = ""
    }
}

@StateObject

@StateObject 是 SwiftUI 中用于办理契合 ObservableObject 协议的目标实例的特点包装器,以保证这些实例的生命周期与当时视图共同( 不短于)。

  • @StateObject 专门用于办理契合 ObservableObject 协议的实例。
  • 标示的目标实例在视图的整个生命周期中保持仅有,即便视图更新,目标实例也不会重新创立。

典型运用场景

  • @StateObject 一般在视图树中最顶层运用,用于创立和保护 ObservableObject 实例。
  • 常用于需求在视图的整个生命周期中继续存在的数据模型或业务逻辑。
  • 相较 @State 而言,@StateObject 更适合办理杂乱的数据模型及其履行逻辑

注意事项

  • @StateObject 触发视图更新的条件包括运用 @Published 标示的特点被赋值( 无论新旧值是否共同 )和调用 objectWillChange 发布者。

  • 只在有必要呼应实例特点改变的视图中运用 @StateObject,假如仅需读取数据而不需求调查改变,可考虑其他选项。

  • 引进 @StateObject 意味着一切相关操作都在主线程上进行( SwiftUI 会隐式为视图添加 @MainActor ),包括异步操作。应将需求在非主线程上运行的代码应该从视图代码中剥离。

struct B:View {
    // 运用 StateObject 后,相当于为当时的视图添加了 @MainActor
    @StateObject var store = Store()
    var body: some View {
        Button("Main Thread"){
            Task.detached{
                await printThradName()
                // output <_NSMainThread: 0x60000170c000>{number = 1, name = main}
            }
        }
    }
    func printThradName() async {
        print(Thread.current)
    }
}
  • 假如在视图存续期有保证的当地创立实例( 比方说 App 层级),且在当时层级也无需呼应该实例中特点的改变,能够不运用 @StateObject
struct DemoApp: App {
    // 因为当时层级的视图的存续期与运用共同,假如当时层级无需呼应 store 改变,能够不必 StateObject
    let store = Store()
    var body: some Scene {
        WindowGroup {
            Test()
                .environmentObject(store)
        }
    }
}

@ObservedObject

@ObservedObject 是 SwiftUI 中用于为视图与 ObservableObject 实例之间创立相关的特点包装器,主要用于在视图存续期内引进外部的 ObservableObject 实例。

  • @ObservedObject 不持有被调查的实例,不保证其生存期。
  • @ObservadObject 能够在视图存续期内切换其所相关的实例。

典型运用场景

  • 一般与 @StateObject 配合运用,父视图运用 @StateObject 创立实例,子视图经过 @ObservedObject 引进该实例,呼应实例改变。
  • 需求动态切换实例的场景。比方在 NavigationSplitView 中,sidebar 中挑选不同的实例,detail 视图动态更换数据源。详情请阅览 StateObject 与 ObservedObject
// 界说一个契合 ObservableObject 协议的数据模型
class DataModel: ObservableObject, Identifiable {
    let id = UUID()
}
struct MyView: View {
    @State private var items = [DataModel(), DataModel()]
    var body: some View {
        VStack {
            // 切换 MySubView 相关的 DataModel 实例
            Button("Replace Model") {
                items.reverse()
            }
            MySubView(model: items.first!)
        }
    }
}
// 子视图
struct MySubView: View {
    // 运用 @ObservedObject 引进外部的 ObservableObject 实例
    @ObservedObject var model: DataModel 
    var body: some View {
        VStack {
            // 显现当时 DataModel 实例的 UUID
            // 当 MyView 中的 'items' 数组改变时,这儿显现的 UUID 会更新,展现了 @ObservedObject 的动态切换才干
            Text(model.id.uuidString)
        }
    }
}
  • 在视图中引进由外部结构或代码来保证存续期的 ObservableObject 实例时运用,例如引进 Core Data 的 NSManagedObject 实例。

注意事项

  • 在 iOS 13 中,因为没有供给 @StateObject ,此时 @ObservedObject 是仅有挑选,或许会因为无法保证实例的存续期而发生 意想不到的成果,为了防止类似问题,能够在更高层级的视图中( 稳定性没有问题的当地 ),经过 @State 来持有该实例,然后在运用的视图中经过 @ObservedObject 来引进。
  • 在引进第三方供给的契合 ObservableObject 实例时,应保证 @ObservedObject 引证的目标在整个视图的生命周期中都是可用的,不然或许导致运行时过错。

@EnvironmentObject

@EnvironmentObject 是用于在当时视图中与上层视图经环境传递的 ObservableObject 实例之间创立相关的特点包装器。它供给了一种便捷的办法在不同的视图层级中引进同享数据,而无需显式地经过每个视图的构造器传递。

典型运用场景

  • 当需求在多个视图间同享同一个数据模型时,如用户设置、主题或运用状况。
  • 适用于构建杂乱的视图层级,其间多个视图需求拜访同一个 ObservableObject 实例。

注意事项

  • 运用 @EnvironmentObject 前,有必要保证已在视图层级的上游供给了相应的实例( 经过 .environmentObject 润饰器 ),不然将导致运行时过错。
  • 它对视图的更新触发条件与 @StateObject@ObservedObject 相同
  • @ObservedObject 相同, @EnvriomentObject 支撑动态切换相关的实例。
struct MyView: View {
    @State private var items = [DataModel(), DataModel()]
    var body: some View {
        VStack {
            Button("Replace Model") {
                // 切换子视图 MySubView 相关的实例
                items.reverse()
            }
            MySubView()
                .environmentObject(items.first!)
        }
    }
}
struct MySubView: View {
    @EnvironmentObject var model: DataModel // 动态切换相关的实例
    var body: some View {
        VStack {
            Text(model.id.uuidString)
        }
    }
}
  • 只在必要时引进 @EnvironmentObject,不然会引发视图不必要的视图更新。一般状况下,会有多个视图从不同层级调查并呼应同一个实例,有必要合理优化才干防止运用性能劣化。这也是许多开发者不喜欢 @EnviromentObject 的原因。
  • 在一个视图层次中,同一个类型的环境目标只要一个实例有用。
@StateObject var a = DataModel()
@StateObject var b = DataModel()
MySubView()
    .environmentObject(a) // 接近视图的有用
    .environmentObject(b)

@Environment

@Environment 是视图用于从环境中读取、呼应、调用特定值的特点包装器。它答应视图拜访由 SwiftUI 或运用环境供给的数据、实例或办法。

典型运用场景

  • 当需求拜访和呼应如界面样式(暗形式/亮形式)、设备方向、字体大小等由体系或上层视图供给的环境值时( 一般对应值类型)。
  • 当需求拜访和调用 SwiftData 的 ModelContext 时(对应引证类型)。
  • 当需求运用体系供给的一些办法时,比方 dismissopenURL( 经过 struct 的 callAsFunction 封装的办法 )。

注意事项

  • 相较于由 @EnvironmentObject 供给的实例所应对的杂乱逻辑,@Environment 引进的数据一般的功用更加的专一。
  • 开发者能够经过自界说 EnvironmentKey 的办法来创立自界说环境值,与体系供给的环境值相同,能够界说各种类型( 值类型、Binding、引证类型、办法的 ),详情请参阅 Custom SwiftUI Environment Values Cheatsheet
public struct ContainerEnvironmentKey: EnvironmentKey {
    // 示例环境键的默认值
    public static var defaultValue = ContainerEnvironment(containerName: "Default")
}
public extension EnvironmentValues {
    var overlayContainer: ContainerEnvironment {
        get { self[ContainerEnvironmentKey.self] }
        set { self[ContainerEnvironmentKey.self] = newValue }
    }
}
  • 在 SwiftUI 中,与 EnvironmentKey 类似的界说办法用途许多,掌握了一种很简单掌握其他的。比方:PreferenceKey( 子视图传递给父视图 )、FocusedValueKey( 基于焦点传递的值 )、LayoutValueKey( 子视图传递给布局容器 )。
  • 因为默认值的存在,@Environment 不会因短少值而导致运用溃散,但由此也简单发生开发者忘掉注入值的状况。
  • @EnvironmentObject 不同,低层级视图不能修正由先人视图传递下来的 EnvironmentValue 的值。
  • 能够经过界说不同的 EnvironmentKey ,在 EnvironmentValue 中创立多个相同类型的不同称号的特点。

总结

  • @StateObject@ObservedObject@EnvironmentObject 专用于相关契合 ObservableObject 协议的实例。
  • 虽然在某些景象下 @StateObject 能够代替 @ObservedObject 并供给类似的功用,但它们各自有共同的运用场景。@StateObject 一般用于创立和保护实例,而 @ObservedObject 用于引进和呼应已存在的实例。
  • 在 iOS 17+ 的环境中,假如运用主要依赖于 Observation 和 SwiftData 结构,那么这三个特点包装器的运用频率或许会相对较低。
  • @State@Environment 不限于只能存储值类型,但也可用于其他类型。
  • @Environment 供给了一种相对更安全的办法来引进环境数据,因为它能够经过 EnvironmentValue 供给默认值。这减少了因遗失数据注入而导致的运用溃散风险。
  • 在 Observation 结构的背景下,@State@Environment 成为了最主要的特点包装器。无论是值类型仍是 @Observable 实例,都能够经过这两种包装器引进视图。
  • 自界说 Binding 供给了强壮的灵活性,答应开发者在数据源和依赖于 Binding 的 UI 组件之间以简练的代码完成杂乱逻辑。

每个特点包装器都有其共同的运用场景和优势。挑选正确的东西关于构建高效、可保护的 SwiftUI 运用是至关重要的。正如在软件开发中常常说到的,没有一种东西是万能的,但恰当地运用它们能够大大进步我们的开发功率和运用质量。

订阅我的电子周报 Fatbobman’s Swift Weekly,你将每周及时获取有关 Swift、SwiftUI、CoreData 和 SwiftData 的最新文章和资讯。

原文发表在我的博客fatbobman.com

欢迎订阅我的公众号:【肘子的Swift记事本】