SwiftUI中常用的Property Wrappers(属性包装器)
在开始之前,力荐一个学习swift/swiftUI的网站:Hacking with Swift
在学到Day 60左右时,我发现SwiftUI中的「“@”打头的一些类似于Java中的Annotation的东西」越来越多,而且都很重要,除了个别几个目前学习中常用的,其他的并不是很清楚,所以在此总结一下。
(本文实例代码多数均来自Apple官方文档)
属性包装器(Property Wrapper)
属性包装器是 Swift 5.1 的新特性之一(详见 SE-0258),其主要作用是将通用的模版代码封装成一种简洁的表达形式,以提高编码的效率。
SwiftUI中内置了大量的属性包装器。熟悉它们的用法和区别,是熟练使用 SwiftUI 的必要条件之一。本部分简要总结了一下目前学到的和见到的属性包装器。后续还会有自定义属性包装器等相关的内容,但是目前水平有限,以后再说吧。(
@State
@State
算是目前用过最多最核心的一个,是 SwiftUI 中最常用的属性包装器之一,用于声明一个状态变量。当状态变量的值发生变化时,SwiftUI 会自动重新渲染视图。Apple Documentation中的官方解释是:“A property wrapper type that can read and write a value managed by SwiftUI.”
需要注意的是,@State
只能修饰简单的数据类型,如 Int
、String
、Bool
、Array
等。如果需要存储复杂的数据类型,可以使用 @StateObject
或 @ObservedObject
,后文会一一再解释。
struct PlayButton: View {
@State private var isPlaying: Bool = false // Create the state.
var body: some View {
Button(isPlaying ? "Pause" : "Play") { // Read the state.
isPlaying.toggle() // Write the state.
}
}
}
- 如果我们想要
@State
一个类的实例,可以将对象的类设置为@Observable
宏:
@Observable
class Library {
var name = "My library of books"
// ...
}
struct ContentView: View {
@State private var library = Library() //这样就可以@State一个实例了
var body: some View {
LibraryView(library: library)
}
}
(在这部分代码中,可能会有一些效率上的问题,具体修改可参考Apple开发文档)
- 如果我们想跨视图传递一个
@State
对象,可以直接将对象引用传递给子视图:
@Observable
class Book {
var title = "A sample book"
var isAvailable = true
}
struct ContentView: View {
@State private var book = Book()
var body: some View {
BookView(book: book)
}
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
@Binding
@Binding
用于将一个属性绑定到另一个属性,以实现数据的双向绑定。在 SwiftUI 中,@Binding
通常与@State
搭配使用,将父视图的状态传递给子视图,并在子视图中修改父视图的状态。
struct PlayerView: View { //父视图
@State private var isPlaying: Bool = false // Create the state here now.
var body: some View {
VStack {
PlayButton(isPlaying: $isPlaying) // Pass a binding.
// ...
}
}
}
struct PlayButton: View { //子视图
@Binding var isPlaying: Bool // Play button now receives a binding.
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
@Bindable
与@Binding
类似,@Bindable
也是用于实现数据的双向绑定。不同的是,@Bindable
作用于可观察(Observable)对象的可变属性(the mutable properties of observable objects)
@Observable
class Book: Identifiable {
var title = "Sample Book Title"
var isAvailable = true
}
struct BookEditView: View {
@Bindable var book: Book
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
TextField("Title", text: $book.title)
Toggle("Book is available", isOn: $book.isAvailable)
Button("Close") {
dismiss()
}
}
}
}
StateObject
@StateObject
也是一个属性包装器,用于声明一个可观察的对象(observable object)。与 @State
不同,@StateObject
用于存储复杂的数据类型,如类的实例。当可观察对象的属性发生变化时,SwiftUI 会自动重新渲染视图。并且,该对象的生命周期由视图控制,当视图被销毁时,对象也会被销毁。
class DataModel: ObservableObject {
@Published var name = "Some Name"
@Published var isEnabled = false
}
struct MyView: View {
@StateObject private var model = DataModel() // Create the state object.
var body: some View {
Text(model.name) // Updates when the data model changes.
MySubView()
.environmentObject(model)
}
}
注意,这里虽然也是可观察对象,但是这里并没有@Observable
。@Observable
通常和@State
配合使用,用法上文已经说过了;而@StateObject
通常和@Published
配合使用,@Published
是一个属性包装器,用于声明一个可观察对象的属性,后面会再具体解释。
ObservedObject
@ObservedObject
也是一个属性包装器,用于声明一个可观察对象(observable object)。与 @StateObject
不同,@ObservedObject
用于在视图中引用其他视图中的可观察对象。
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func haveBirthday() -> Int {
age += 1
return age
}
}
let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"
- 看起来
@StateObject
和@ObservedObject
很是相似,那么他们二者的区别?
@ObservedObject
和 @StateObject
的使用是完全一样的,唯一的区别在于它们的生命周期不同。
@StateObject
用于在视图内部创建一个对象,并且该对象的生命周期由视图控制,当视图被销毁时,对象也会被销毁。@StateObject
确保对象只会被创建一次,即使视图被多次刷新也是如此。
使用 @StateObject
的场景:
1.当视图需要拥有并控制一个对象的状态时。
2.当对象需要与视图的生命周期保持一致时。
3.当对象包含与视图状态相关的数据时。
@ObservedObject
用于在视图中引用其他对象。与 @StateObject
不同,@ObservedObject
不会创建或控制对象的生命周期。相反,它只是允许视图订阅对象的更改。
使用 @ObservedObject
的场景:
1.当视图需要订阅另一个对象的更改时。
2.当对象由视图外部创建和管理时。
3.当对象的状态与视图无关时。
所以简单来讲,@StateObject
在视图内部创建并控制对象的生命周期,而ObservedObject
则是在视图中引用其他对象。
虽然多数情况下都是使用 @StateObject
,但是理解它和 @ObservedObject
的区别,有助于在需要时选择合理的方法实现功能。
@Published
上文已经提到过了,@Published
用于标记可观察对象的属性。当标记为 @Published
的属性发生更改时,它会自动通知所有观察该属性的视图,并触发视图更新。
class Weather {
@Published var temperature: Double
init(temperature: Double) {
self.temperature = temperature
}
}
let weather = Weather(temperature: 20)
cancellable = weather.$temperature
.sink() {
print ("Temperature now: \($0)")
}
weather.temperature = 25
// Prints:
// Temperature now: 20.0
// Temperature now: 25.0
- 还有一点需要注意:@Published 实际上是
Combine
framework 框架的Publisher
protocol 协议的语法糖,比如@Published var str = ""
中的str
是String
类型,而$str
的类型是Published<String>.Publisher
,实际上就是一个Publisher
实例,系统提供的Publisher
实例方法它都是可以调用的。
其它的属性包装器基本是类似的,比如 @Binding var value: String
, value
是 String
类型,而 $value
是 Binding<String>
类型。
- 在 WWDC23 Swift 5.9 版本中,SwiftUI 不再使用
Combine
,而是改用新的Observation
框架,提供Observable
协议,上文有提到并作了解释。
@Environment
@Environment 可以在任何视图中访问系统预设的环境变量,比如是否暗黑模式、系统日历、时区等。
更多系统预设的环境变量请参考:EnvironmentValues。
@Environment(\.colorScheme) private var colorScheme
LabeledContent("ColorScheme") {
Text(colorScheme == .light ? "Light" : "Dark")
}
@EnvironmentObject
@EnvironmentObject
用于在应用程序中的视图之间共享可观察对象,使其可以轻松地从视图层次结构中的任何位置访问和更新共享数据,而无需手动将其传递下来。
class User: ObservableObject {
@Published var name = "Taylor Swift"
}
struct EditView: View {
@EnvironmentObject var user: User
var body: some View {
TextField("Name", text: $user.name)
}
}
struct DisplayView: View {
@EnvironmentObject var user: User
var body: some View {
Text(user.name)
}
}
@Environment
和@EnvironmentObject
虽然长得很像,但其实很不一样:
1.用途不同:
@EnvironmentObject
: 用于在视图之间共享可观察对象。
@Environment
: 用于从环境中访问 SwiftUI 或应用程序提供的全局数据、实例或方法。
2.访问类型不同:
@EnvironmentObject
: 只能访问可观察对象。
@Environment
: 可以访问各种类型的数据,包括值类型、引用类型、函数等。
3.数据来源不同:
@EnvironmentObject
: 可观察对象由用户显式提供。
@Environment
: 环境数据由 SwiftUI 或应用程序提供,例如屏幕尺寸、用户界面风格等。
用哪个?
上面说的很多,@Binding
& @Bindable
, @Observable
, @StateObject
& @ObeservedObject
, @EnvironmentObject
等,这些都和视图或结构之间的传值相关。
- 那么该如何选择呢?
(这个我自己也有点迷惑……
但比较确定的是,@Bindable
和 Observable
是 WWDC 23 中新推出的 Swift 5.9 版本中的新的 Observation
框架中的内容,是新的东西,而 @StateObject
,@ObeservedObject
等就是相对较为老的老必登了,并且加上 SwiftUI 最新版本开始不再使用 Combine
框架,最后具体用哪个取决于你要用的版本。如果想要体验最新的功能和框架,@Bindable
和 Observable
肯定是最省事也是最“遥遥领先”的选择。
(所以,Swift是一个正在蓬勃发展的活跃的语言,一定要关注每年 Apple 的 微微的叉(划掉)WWDC!一定要看!
@Codable
@Codable
,用于将 Swift 类型编码和解码为数据格式,例如 JSON 或 plist。它简化了在 Swift 本地表示和适合外部存储或通信的结构化格式之间转换数据的过程。
@Query
@Query
,适用于 WWDC 23 公布的新框架 SwiftData,是一种使用指定标准获取模型(model)并管理这些模型的类型,以便它们与基础数据保持同步。简单来说,就是使用 SwiftData 存储数据时,使用 @Query
获取存储的数据,并且可以进行一些排序操作。
以下是我在学习过程中写过的项目代码中相关的部分:
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Query(sort: \ExpenseItem.amount, order: .reverse) var expenseItems: [ExpenseItem]
var body: some View {
AddView(expenseItems: expenseItems)
}
}
struct AddView: View {
var body: some View {
Button("Done") {
let item = ExpenseItem(name: "Chino", type: "daughter", amount: 1)
modelContext.insert(item)
dismiss()
print(amount ?? 114514)
}
}
}
@Model
class ExpenseItem {
var id = UUID()
let name: String
var type: String
var amount: Double
init(id: UUID = UUID(), name: String, type: String, amount: Double) {
self.id = id
self.name = name
self.type = type
self.amount = amount
}
}
@AppStorage
主要用于数据持久化,并且能让我们轻松地将少量数据存储在用户的默认设置(UserDefaults)中。此外,当这些数据变更时,相关联的视图会自动进行更新。
struct ContentView: View {
@AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
Text("Welcome, \(username)!")
Button("Log in") {
username = "@twostraws"
}
}
}
}
宏(Macro)
Macro宏是一个更新的东西,是WWDC23,Swift 5.9版本中引入的新特性,我掌握的部分就更少了(
这里只暂时列举我遇到过的Macro宏及其用途。如果想要更深的了解学习可以参考这篇文章。
@Observable
@Observable
是一个Macro宏,用于声明一个可观察对象。可观察对象是一个具有可变属性的类,当属性发生变化时,SwiftUI 会自动重新渲染视图。
@Observable
class Car {
var name: String = ""
var needsRepairs: Bool = false
init(name: String, needsRepairs: Bool = false) {
self.name = name
self.needsRepairs = needsRepairs
}
}
@Model
是一种专用于 SWiftData 框架的宏,可以将一个 Swift 类转化为一个「被SwiftData管理的一个存储模型(stored model)」。简单说就是想让一个类被SwiftData管理和存储,就加上 @Model
宏就好了。
上文中 @Query
部分已经出现了,这里再简单写一下用法示例:
@Model
class ExpenseItem {
var id = UUID()
let name: String
var type: String
var amount: Double
init(id: UUID = UUID(), name: String, type: String, amount: Double) {
self.id = id
self.name = name
self.type = type
self.amount = amount
}
}
@Transient
同样是适用于 SwiftData 框架的宏,作用是告诉SwiftData在管理拥有类时不要保留注释属性。
听起来很疑惑?我也很疑惑。让我们看看实例代码:
@Model
class Player {
var name: String
var score: Int
@Transient
var levelsPlayed = 0
init(name: String, score: Int) {
self.name = name
self.score = score
}
}
SwiftData会自动将所有 @Model
类中的属性保存到其数据存储中。如果您不想这样——如果有仅在程序运行时需要的临时数据——那么就可以使用 @Transient
宏进行标记,以便 SwiftData 将其视为短暂和一次性的属性,这样它就不会与其他数据一起保存。
- 需要注意的是,默认情况下,SwiftData 会将计算属性视为“Transient”的,所以就不需要单独去添加这个Macro宏批注了。
@Attribute
还是一个专门适用于 SwiftData 框架的宏,可以用来给 @Model
类中的属性增加自定义的行为。
Apple Documentation中的官方解释是:“Specifies the custom behavior that SwiftData applies to the annotated property when managing the owning class.”
@Model
class RemoteImage {
@Attribute(.unique) var sourceURL: URL
var data: Data
init(sourceURL: URL, data: Data = Data()) {
self.sourceURL = sourceURL
self.data = data
}
}
Swiftdata框架管理 Model 类存储属性的默认行为适用于大多数用例。但是,如果需要更改特定属性的持久性行为,可以使用 @Attribute
宏进行注释。例如上方的代码中,会可能希望通过指定属性的值在该模型的所有实例中都是唯一的,那么就可以添加 @Attribute(.unique)
从而避免模型数据中的冲突。
@Relationship
又又又是一个只适用于 SwiftData 框架的 Macro宏,用于指定一个属性,使其成为两个 Model 类之间的联系(Relationship)。
如果模型类的一个或多个属性表示其所在的模型与另一个模型之间的关系,可以使用 @Relationship
宏注释这些属性。这能够使SwiftData在运行时强制执行这些关系——包括删除相关数据,以及将任何相关元数据写入持久存储,以便跨应用程序启动之间存在关系。
在以下示例中,远程图像可能属于一个分类,并且一个类别可以包含零个、一个或多个图像。
@Model
class RemoteImage {
@Attribute(.unique) var sourceURL: URL
@Relationship(inverse: \Category.images) var category: Category?
var data: Data
init(sourceURL: URL, data: Data = Data()) {
self.sourceURL = sourceURL
self.data = data
}
}
@Model
class Category {
@Attribute(.unique) var name: String
@Relationship var images = [RemoteImage]()
init(name: String) {
self.name = name
}
}
如有错误,请及时指出~ 欧内盖!