- @states
- @Binding
- ObservableObject,@ObservedObject
- @EnvironmentObject
在学习上面的装饰器之前,首先要了解@propertyWrapper,因为它们都是由@propertyWrapper而来。
@PropertyWrapper
官方介绍如下
A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.
属性包装器在管理属性存储方式的代码和定义属性的代码之间添加了一层分隔。例如,如果您有提供线程安全检查或将其基础数据存储在数据库中的属性,则必须在每个属性上编写该代码。使用属性包装器时,在定义包装器时只需编写一次管理代码,然后通过将其应用于多个属性来重用该管理代码
使用@propertyWrapper产生一个属性包装器:@XXX,使用这个@XXX 对属性进行包装
包装前我想向你介绍几个创建包装器的时需要实现的属性:wrappedValue(被包装的值), projectedValue(呈现值)
包装器大概这样产生
@propertyWrapper
struct 包装器名称 {
var wrappedValue: 类型 {
get {}
set {}
}
var projectedValue: 类型 {
return
}
初始化方法(可选)
}
- 其中wrappedValue就是需要包装的那个值,直接访问,projectedValue就是你可能希望通过包装额外呈现出来的某个值,$+属性名称访问(下面有例子)
//我想创造一个体温温度包装器,使用该包装器包装的变量总是小于46.5且大于35(假定温度计的极限是35-46.5度),同时呈现K氏度以供参考
@propertyWrapper
struct Temperature {
var celcius = 35.0//摄氏温度
var wrappedValue: Double {
get {
return celcius
}
set {
celcius = newValue <= 35 ? 35 : min(46.5, newValue)//如果小于等于35就显示35度,否则在46.5和设置的温度中取小
}
}
var projectedValue: Double {
return celcius + 273.15//K氏度= 摄氏度+ 273.15
}
}
struct People {
@Temperature(celcius: 36.5) var templete
}
let people = People()
print("体温:",people.templete,"K氏度:",people.$templete)
//体温: 36.5 K氏度309.65
@State
这个只要记住一句话:被@State包装的值发生改变时,UI将会被通知刷新
个人猜测底层在set方法中对UI进行了更新
因为struct中是不能修改属性的,要想修改属性,可以添加@state注解
import SwiftUI
struct ContentView: View {
@State var x = "HelloWorld"
var body: some View {
VStack{
Text(x)
Button(action:{self.x = "Hello UI"}){
Text("ChangeValue")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@Binding
双向绑定
这里写一个输入框的例子, TextField第二个参数是一个Binding
struct ContentView: View {
@State private var name = ""
var body: some View {
Form {
TextField("Enter your name", text: $name)
Text("Hello \(name)")
}
}
}
可以自定义双向绑定
struct ContentView: View {
@State private var username = ""
var body: some View {
let binding = Binding(
get: { self.username },
set: { self.username = $0 }
)
return VStack {
TextField("Enter your name", text: binding)
Text(username)
}
}
}
@Binding主要是为了解决@state不能在对象间传递的问题。
它做的事情是将值语义的属性“转换”为引用语义。对被声明为@Binding的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递
ObservableObject,@ObservedObject
@ObservedObject 的用处和 @State 非常相似,从名字看来它是来修饰一个对象的,这个对象可以给多个独立的 View 使用。如果你用 @ObservedObject 来修饰一个对象,那么那个对象必须要实现 ObservableObject 协议,然后用 @Published 修饰对象里属性,表示这个属性是需要被 SwiftUI 监听的
final class PodcastPlayer: ObservableObject {
@Published private(set) var isPlaying: Bool = false
func play() {
isPlaying = true
}
func pause() {
isPlaying = false
}
}
我们定义了一个 PodcastPlayer 类,这个类可以给不同的 View 使用,SwiftUI 会追踪使用 View 里经过 @ObservableObject 修饰过的对象里进过 @Published 修饰的属性变换,一旦发生了变换,SwiftUI 会更新相关联的 UI
struct EpisodesView: View {
@ObservedObject var player: PodcastPlayer
let episodes: [Episode]
var body: some View {
List {
Button(
action: {
if self.player.isPlaying {
self.player.pause()
} else {
self.player.play()
}
}, label: {
Text(player.isPlaying ? "Pause": "Play")
}
)
ForEach(episodes) { episode in
Text(episode.title)
}
}
}
}
@EnvironmentObject
从名字上可以看出,这个修饰器是针对全局环境的。通过它,我们可以避免在初始 View 时创建 ObservableObject, 而是从环境中获取 ObservableObject
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let window = UIWindow(frame: UIScreen.main.bounds)
let episodes = [
Episode(id: 1, title: "First episode"),
Episode(id: 2, title: "Second episode")
]
let player = PodcastPlayer()
window.rootViewController = UIHostingController(
rootView: EpisodesView(episodes: episodes)
.environmentObject(player)
)
self.window = window
window.makeKeyAndVisible()
}
}
struct EpisodesView: View {
@EnvironmentObject var player: PodcastPlayer
let episodes: [Episode]
var body: some View {
List {
Button(
action: {
if self.player.isPlaying {
self.player.pause()
} else {
self.player.play()
}
}, label: {
Text(player.isPlaying ? "Pause": "Play")
}
)
ForEach(episodes) { episode in
Text(episode.title)
}
}
}
}
可以看出我们获取 PodcastPlayer 这个 ObservableObject 是通过 @EnvironmentObject 修饰器,但是在入口需要传入 .environmentObject(player) 。@EnvironmentObject 的工作方式是在 Environment 查找 PodcastPlayer 实例。
@Environment
继续上面一段的说明,我们的确开一个从 Environment 拿到用户自定义的 object,但是 SwiftUI 本身就有很多系统级别的设定,我们开一个通过 @Environment 来获取到它们
struct CalendarView: View {
@Environment(\.calendar) var calendar: Calendar
@Environment(\.locale) var locale: Locale
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
return Text(locale.identifier)
}
}
通过 @Environment 修饰的属性,我们开一个监听系统级别信息的变换,这个例子里一旦 Calendar, Locale, ColorScheme 发生了变换,我们定义的 CalendarView 就会刷新
总结
- @State 修饰的属性在被改变后,ui会自动刷新。当不适合在对象间传递
- 双向绑定:属性值的变动会刷新ui,同时ui上的操作也会将值实时赋给属性
- ObservableObject: 适合多个对象使用
- @EnvironmentObject:适合全局变量配置
如果你需要在多个 View 中共享数据,@State可能不是很好的选择;如果还需要在 View 外部操作数据,那么 @State 甚至就不是可选项了。含有少数几个成员变量的值类型,也许使用 @State 也还不错。但是对于更复杂的情况,例如含有很多属性和方法的类型,可能其 中只有很少几个属性需要触发 UI 更新,也可能各个属性之间彼此有关联,那 么我们应该选择引用类型和更灵活的可自定义方式。
属性 | 参数类型 | 具体描述 |
---|---|---|
timingFunction | CAMediaTimingFunction | 动画缓冲属性 |
delegate | id |
动画代理属性 |
removedOnCompletion | BOOL | 动画完成后是否从图层上移除 |