什么是 @State 属性包装器?
From: github.com/jaywcjlove/...
SwiftUI 使用 @State 属性包装器允许我们修改结构体中的值,由于结构体是值类型,通常不允许这样做。
当我们将 @State 放在属性之前时,我们有效地将其存储从结构中移出,并移到 SwiftUI 管理的共享存储中。 这意味着SwiftUI可以在需要时销毁并重新创建我们的结构(这可能会发生很多!),而不会丢失其存储的状态。
@State 应该与简单的结构类型(例如String,Int和数组)一起使用,并且通常不应与其他视图共享。 如果要在视图之间共享值,则可能应该使用 @ObservedObject 或 @EnvironmentObject - 两者都将确保在数据更改时刷新所有视图。
为了增强 @State 属性的本地性,Apple 建议您将其标记为私有,如下所示:
java
@State private var username = ""
这不是必需的,但似乎是明智的做法。
提示:如果需要,您可以使用 @State 跟踪引用类型,只是在引用类型更改时不会收到通知。 这对于不符合 ObservableObject 协议的类特别有用。
什么是 @StateObject 属性包装器?
FROM: github.com/jaywcjlove/...
SwiftUI 的 @StateObject 属性包装器旨在填补状态管理中的一个非常具体的空白:当您需要在其中一个视图中创建引用类型并确保该类型在该视图以及与之共享的其他视图中仍然有效时,就可以使用。
例如,考虑一个简单的 User 类,例如:
kotlin
class User: ObservableObject {
var username = "@twostraws"
}
如果要在各种视图中使用它,则需要在 SwiftUI 外部创建它并将其注入,或者在其中一个 SwiftUI 视图中创建它并使用 @StateObject,如下所示:
sql
struct ContentView: View {
@StateObject var user = User()
var body: some View {
Text("Username: (user.username)")
}
}
这将确保视图更新时不会破坏 User 实例。
以前,您可能曾经使用 @ObservedObject 来获得相同的结果,但这很危险--有时且仅在某些情况下,@ObservedObject 可能会意外释放其存储的对象,因为它并不是被设计为最终的真理来源。 目的。 @StateObject 不会发生这种情况,因此您应该改用它。
重要说明:每个对象只能使用 @StateObject 一次,无论在哪个视图中创建对象都应使用 @StateObject。 共享对象的所有其他视图应使用@ObservedObject。
什么是 @Published 属性包装器?
@Published 是 SwiftUI 中最有用的属性包装器之一,它使我们能够创建可观察的对象,这些对象会在发生更改时自动宣布。 SwiftUI 将自动监视此类更改,并重新调用所有依赖于数据的视图的 body 属性。
实际上,这意味着只要更改带有标记为 @Published 的属性的对象,就会重新加载使用该对象的所有视图以反映这些更改。
例如,如果我们有一个这样的可观察对象:
kotlin
class Bag: ObservableObject {
var items = [String]()
}
这符合 ObservableObject 协议,这意味着 SwiftUI 的视图可以监视它的更改。 但是,由于它的唯一属性未标记为 @Published,因此不会发送任何更改通知-您可以将项目自由添加到数组中,而视图也不会更新。
如果您希望每当添加或删除项目时都发送变更公告,则可以使用 @Published 进行标记,如下所示:
kotlin
class Bag: ObservableObject {
@Published var items = [String]()
}
您无需执行其他任何操作 - @Published 属性包装器可以有效地将 willSet 属性观察者添加到项目中,这样任何更改都会自动发送给观察者。
如您所见,@Published 是选择加入的--您需要列出应该引起公告的属性,因为默认情况下,更改不会导致重新加载。 这意味着您可以拥有存储缓存的属性,供内部使用的属性等,并且它们不会强制 SwiftUI 在更改视图时重新加载视图,除非您用 @Published 专门标记它们。
什么是 @ObservedObject 属性包装器?
SwiftUI 为我们提供了 @ObservedObject 属性包装器,以便视图可以监视外部对象的状态,并在重要内容发生变化时得到通知。 它的行为与 @StateObject 相似,不同之处在于它不得用于创建对象--仅将 @ObservableObject 与在其他位置创建的对象一起使用,否则 SwiftUI 可能会意外破坏该对象。
例如,我们可能会使用以下内容:
kotlin
class Order: ObservableObject {
@Published var items = [String]()
}
struct ContentView: View {
@ObservedObject var order: Order
var body: some View {
// your code here
}
}
该 Order 类使用 @Published,因此它将在项目更改时自动发送更改公告,而 ContentView 使用 @ObservedObject 来监视那些公告。如果没有 @ObservedObject,则更改通知将被发送但被忽略。
尽管这看起来很简单,但值得深入探讨一些细节。
首先,用 @ObservedObject 标记的任何类型都必须符合 ObservableObject 协议,这反过来意味着它必须是类而不是结构。这不是可选的 -- SwiftUI 要求我们在此处使用一个类。
其次,观察到的对象是专门为视图外部的数据设计的,这意味着它们可能在多个视图之间共享。 @ObservedObject 属性包装器将自动确保密切监视该属性,以便重要的更改将重新使用该视图加载任何视图。这也意味着必须在其他位置创建数据,然后将其发送到您的视图中。
第三,并不是观察对象中的所有属性都会导致视图刷新-您需要使用@Published或自定义公告来决定哪些属性应发送更改通知。为符合 ObservableObject 的类型提供默认的 objectWillChange 发布者,以便根据需要进行自定义声明。
什么是 @EnvironmentObject 属性包装器?
SwiftUI 的 @EnvironmentObject 属性包装器使我们可以创建依赖共享数据的视图,这些视图通常跨整个 SwiftUI 应用程序。 例如,如果创建一个将在应用程序的许多部分之间共享的用户,则应使用 @EnvironmentObject。
例如,我们可能有一个像这样的 Order 类:
kotlin
class Order: ObservableObject {
@Published var items = [String]()
}
这符合 ObservableObject,这意味着我们可以将其与 @ObservedObject 或 @EnvironmentObject 一起使用。 在这种情况下,我们可以创建一个将其与 @EnvironmentObject 一起使用的视图,如下所示:
less
struct ContentView: View {
@EnvironmentObject var order: Order
var body: some View {
// your code here
}
}
请注意, order 属性没有给定默认值 - 通过使用 @EnvironmentObject ,我们表明该值将由 SwiftUI 环境提供,而不是由此视图显式创建。
@EnvironmentObject 与 @ObservedObject 有很多共同点:两者都必须引用符合 ObservableObject 的类,都可以在许多视图之间共享,并且都可以在发生重大更改时更新正在监视的所有视图。 但是,@EnvironmentObject 明确表示"此对象将由某个外部实体提供,而不是由当前视图创建或专门传递。
实际上,假设您是否拥有 视图A,并且视图A拥有视图E所需的一些数据。使用 @ObservedObject,视图A需要将对象传递给视图B,然后将其传递给视图C,然后传递给视图D,最后传递给视图E -- 所有中间视图都需要发送给对象,即使它们实际上并没有需要它。
使用 @EnvironmentObject 时,视图A可以创建一个对象并将其放置到环境中。然后,只要需要,它内的任何视图都可以访问该环境对象,而不必显式传递它 -- 这使我们的代码更加简单。
警告:显示使用 @EnvironmentObject 的视图时,SwiftUI 将立即在环境中搜索正确类型的对象。如果找不到此类对象(即,如果您忘记将其放置在环境中),则您的应用将立即崩溃。当使用 @EnvironmentObject 时,可以有效地保证对象在需要时会存在于环境中,这有点像使用隐式解包的可选对象。
什么是 @Environment 属性包装器?
SwiftUI 给我们提供了 @Environment 和 @EnvironmentObject 属性包装器,但是它们有些微的不同:@EnvironmentObject 允许我们向环境中注入任意值,而 @Environment 专门用于处理 SwiftUI 自己的预定义键。
例如,@Environment 非常适合读取诸如 Core Data 受管对象上下文之类的信息,设备是处于暗模式还是亮模式,使用何种尺寸类渲染视图以及更多--来自系统的固定属性 。 在代码中,它看起来像这样:
less
@Environment(.horizontalSizeClass) var horizontalSizeClass
@Environment(.managedObjectContext) var managedObjectContext
另一方面,@EnvironmentObject 设计用于从环境中读取任意对象,如下所示:
sql
@EnvironmentObject var order: Order
这种差异听起来很小,但由于实现 @EnvironmentObject 的方式而非常重要。 当我们说 order 的类型是 Order 时, SwiftUI 将在其环境中查找该类型的对象并将其附加到 order 属性。 但是,使用 @Environment 时,无法实现相同的行为,因为许多事物可能共享相同的数据类型。
例如:
less
@Environment(.accessibilityReduceMotion) var reduceMotion
@Environment(.accessibilityReduceTransparency) var reduceTransparency
@Environment(.accessibilityEnabled) var accessibilityEnabled
所有这三个环境键都返回一个布尔值,因此,如果不指定确切的键,则意味着无法正确读取它们。
什么是 @Binding 属性包装器?
@Binding 让我们声明一个值实际上来自其他地方,并且应该在两个地方共享。 这与 @ObservedObject 或 @EnvironmentObject 不同,这两个对象都是为在可能的许多视图之间共享引用类型而设计的。
例如,我们可能有一个带有 @State 属性的 ContentView,该属性存储是否显示子视图,如下所示:
swift
struct ContentView: View {
@State private var showingAddUser = false
var body: some View {
VStack {
// your code here
}
}
.sheet(isPresented: $showingAddUser) {
// show the add user view
}
}
它使用 showingAddUser 作为工作表的 isPresented 参数,这意味着当该布尔值变为 true 时,将显示添加用户视图。 但是,如果需要,我们如何才能允许添加用户视图自行关闭-例如,如果用户点击"完成"按钮?
我们想要发生的是,将添加用户视图的 showingAddUser 设置回 false,这将导致 ContentView 将其隐藏。 这正是 @Binding 的目的:它使我们可以在添加用户视图中创建一个属性,该属性表示"此值将从其他地方提供,并将在我们与其他地方之间共享"。
因此,我们可以创建一个添加用户视图,如下所示:
swift
struct AddView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Dismiss") {
isPresented = false
}
}
}
该属性的字面意思是"我有一个名为 isPresented 的布尔值,但它存储在其他位置。" 因此,当我们创建该 AddView 来替换 // show the add user view,我们需要提供该值以便可以对其进行操作:
php
.sheet(isPresented: $showingAddUser) {
AddView(isPresented: $showingAddUser)
}
这使 ContentView 和 AddView 可以共享相同的布尔值 - 当它在一个位置更改时,它在另一个位置也更改。
什么是 @GestureState 属性包装器?
SwiftUI 为我们提供了一个用于跟踪手势状态的特定属性包装器,称为 @GestureState。 尽管您可以使用简单的 @State 属性包装器完成相同的操作,但 @GestureState 具有附加功能,即当手势结束时,它会自动将属性设置回其初始值,并且通常比使用简单的 @State 快得多 。
例如,我们可能想要创建一个可以拖动视图的手势。 为此,我们首先需要创建一个 @GestureState 属性,以存储视图已移动了多少,如下所示:
swift
@GestureState var dragAmount = CGSize.zero
该参数的默认值为 CGSize.zero,这意味着手势结束时它将自动返回该值。
接下来,我们将附加一个 offset() 修饰符,以使我们的视图可以通过 dragAmount 中的任何值移动:
scss
Image("niagara-falls")
.offset(dragAmount)
最后,我们将绑定到我们的 dragAmount 属性的手势,如下所示:
scss
Image("niagara-falls")
.offset(dragAmount)
.gesture(
DragGesture().updating($dragAmount) { value, state, transaction in
state = value.translation
}
)
里面有很多代码,让我们逐步解释:
DragGesture().updating()代码创建一个新的拖动手势,要求它修改存储在dragAmount中的值-这就是我们的CGSize。- 它需要一个带有三个参数的闭包:
value,state和transaction。 value参数是拖动的当前数据-拖动的开始位置,移动的距离,预计的结束位置等等。state参数是一个inout值,这是我们的属性。因此,除了直接读取或写入dragAmount之外,我们还应该修改状态。transaction参数是一个inout值,用于存储整个动画上下文,从而为我们提供了一些有关正在发生的情况的信息,例如,这是连续动画还是瞬态动画。连续动画可以通过拖动滑块来产生,而瞬态动画可以通过点击按钮来产生。- 为了使我们的视图可拖动,我们要做的就是将当前转换直接分配给状态拖动(在这种情况下,它实际上是dragAmount),然后在
offset()修饰符中使用它来移动视图。
请记住,@GestureState 的优点之一是,当手势结束时,它会自动将属性的值设置回其初始值。在这种情况下,这意味着我们可以在所需的所有位置上拖动视图,放开后,它将立即回到其原始位置。
什么是 @FetchRequest 属性包装器?
SwiftUI 为我们提供了专用的属性包装器,用于处理 Core Data 提取请求,它使我们可以直接将数据嵌入到 SwiftUI 视图中,而无需编写额外的逻辑。
您必须为 @FetchRequest 提供至少两个值:要读取的实体,以及用于排列数据的任何排序描述符。 您还可以选择提供谓词以根据需要过滤数据。
重要提示:在使用 @FetchRequest 之前,您必须首先将 Core Data 托管对象上下文注入到环境中--有关如何执行此操作的说明,请参见如何从 SwiftUI 视图访问 Core Data 托管对象上下文。
作为一个基本示例,我们可以显示来自 Core Data 上下文的所有用户,如下所示:
less
@FetchRequest(
entity: User.entity(),
sortDescriptors: []
) var users: FetchedResults<User>
这不会对数据进行排序,因此将按照添加顺序返回用户。@FetchRequest 自动包含 @ObservedObject,因此,如果您在List,ForEach或类似文件中使用数据,则当基础数据发生更改时,它将自动刷新。
提示:我将 @FetchRequest 代码分成几行,以使其更易于阅读,但这不是必需的。
如果要对数据进行排序,请以键路径数组的形式提供排序描述符,如下所示:
less
@FetchRequest(
entity: User.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \User.name, ascending: false)
]
) var users: FetchedResults<User>
您可以根据需要提供任意数量的内容,它们将按顺序进行评估。
要同时添加 predicate,请使用以下格式创建 NSPredicate:
less
@FetchRequest(
entity: User.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \User.name, ascending: false),
],
predicate: NSPredicate(format: "surname == %@", "Hudson")
) var users: FetchedResults<User>
什么是 @AppStorage 属性包装器?
SwiftUI 具有专用的属性包装程序,可用于从 UserDefaults 中读取值,当值更改时,该程序将自动重新调用视图的 body 属性。 也就是说,此包装器有效地监视 UserDefaults 中的键,并且如果该键更改,将刷新您的UI。
例如,这将监视 UserDefaults 中的 username 键,该键将在按下按钮时设置:
scss
struct ContentView: View {
@AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
Text("Welcome, (username)!")
Button("Log in") {
username = "@twostraws"
}
}
}
}
在上面更改 username 将导致新字符串立即写入 UserDefaults,同时还会更新视图。 如果我们使用了较旧的方法,那也将是正确的:
c
UserDefaults.standard.set("@twostraws", forKey: "username")
@AppStorage 默认情况下将监视 UserDefaults.standard,但是如果您愿意,也可以使其监视特定的应用程序组,如下所示:
less
@AppStorage("username", store: UserDefaults(suiteName: "group.com.hackingwithswift.unwrap")) var username: String = "Anonymous"
重要:@AppStorage 将您的数据写入 UserDefaults,这不是安全存储。 因此,您不应使用 @AppStorage 保存任何个人数据,因为它相对容易提取。
什么是 @SceneStorage 属性包装器?
如果您想为每个屏幕保存唯一的数据,则应使用 SwiftUI 的 @SceneStorage 属性包装器。 它的工作方式类似于 @AppStorage,为您提供了一个名称来保存内容以及一个默认值,但是它不是使用 UserDefaults 而是用于状态恢复 - 甚至可以与多种复杂的多场景一起使用 iPadOS 中我们经常看到的设置。
例如,如果您有一个文本编辑器并想存储用户输入的内容,则应使用以下代码:
arduino
struct ContentView: View {
@SceneStorage("text") var text = ""
var body: some View {
#if os(iOS)
NavigationView {
TextEditor(text: $text)
}
.navigationViewStyle(StackNavigationViewStyle())
#else
NavigationView {
TextEditor(text: $text)
}
#endif
}
}
注意:我在此处使用了 StackNavigationViewStyle,因为它迫使 iPad 将所有空间分配给我们的文本编辑器。
因为它使用 @SceneStorage,所以 SwiftUI 将自动确保每个场景实例都有其自己的文本副本 -- 如果同时运行应用程序,则两者都会正确保存和恢复其数据。
现在,在使用 @SceneStorage 之前,Apple有一些重要警告:
- 不要保存大量数据; 保存状态恢复所需的一切。
- 永远不要将敏感数据存储在场景存储中,因为它不安全。
- 如果用户转到应用程序切换器并销毁了您的应用程序,则场景存储也将被销毁。
什么是 @ScaledMetric 属性包装器?
SwiftUI 为我们提供了 @ScaledMetric 属性包装器,以定义应根据用户的动态类型设置自动缩放的数字。
在最基本的用法中,为您的属性提供默认值,然后 @ScaledMetric 将完成其余工作。 例如,这将根据用户的设置以不同的大小绘制同一张图片:
css
struct ContentView: View {
@ScaledMetric var imageSize: CGFloat = 100
var body: some View {
Image(systemName: "cloud.sun.bolt.fill")
.resizable()
.frame(width: imageSize, height: imageSize)
}
}
如果需要缩放以与其他特定文本匹配,则可以为属性包装器使用 relativeTo 参数,该参数可让您指定要匹配的字体大小。 例如,这将与较大的标题一起缩放:
java
@ScaledMetric(relativeTo: .largeTitle) var imageSize: CGFloat = 100
什么是 @UIApplicationDelegateAdaptor 属性包装器?
如果需要访问 SwiftUI 中的 AppDelegate 功能,则应创建一个继承自 NSObject 和 UIApplicationDelegate 的类,并为其提供所需的任何功能,例如:
kotlin
class AppDelegate: NSObject, UIApplicationDelegate {
// add app delegate methods here
}
例如,如果您想实现旧的 didFinishLaunchingWithOptions 方法,则可以使用以下方法:
swift
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Your code here")
return true
}
}
一旦有了,在主应用程序中使用 UIApplicationDelegateAdaptor 属性包装器,以便 SwiftUI 知道创建和管理您的应用程序委托类:
scss
@main
struct NewIn14App: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}