由donnywals于2020年6月8日发布 属性包装器是Swift 5.1中引入的一项功能,它们在SwiftUI和Combine中发挥了巨大作用,这是iOS 13中与Swift 5.1一起提供的两个框架。社区很快创建了一些有用的示例,这些示例很快就被人们所接受。
作为属性包装器的用户,您不必担心它们的确切含义或工作方式。 您只需要知道如何使用它们即可。 但是,如果您好奇属性包装器如何在内部工作,这就是适合您的文章。
我想深入研究Property Wrappers,并带您探索它们的工作原理。Property Wrappers的应用示例,可以参考《SwiftUI从入门到实战》第10到17节:
10. 如何使用@Binding绑定包装关闭模态窗口 11. 如何使用@ObservedObject监听实例对象一 12. 如何使用@ObservedObject监听实例对象二 13. 如何使用@StateObject实现简单的购物车功能 14. 使用@EnvironmentObject进行页面间的数据传递 15. 使用@Environment访问环境中的指定key的值 16. 使用@AppStorage将属性的值同步到UserDefaults 17. 使用@SceneStorage存储各个场景的状态
Swift的发展建议
至少可以说,Swift中关于属性包装器的提案是一个有争议的提案。 此功能最初是由“property delegates”创造的,但在最初的审核中未能实现。 它对最初的提案进行了三处修订,以使属性包装器得到社区的接受。
最终,社区同意了第四版属性包装器提案。
在提出属性包装器之前,乔·格罗夫(Joe Groff)提出了一项名为``Property Behaviors''的功能。 这发生在2016年,即引入属性包装器的三年之前。
我们可以从中得出的结论是,属性包装器是一个已经存在很长时间的功能,并且基于最初建议的长度和透彻性,我认为可以肯定地说,属性包装器的设计已经花费了很多年。 在最终提案被Swift 5.1接受并实施之前,它逐渐成熟。
为什么我们需要属性包装器?如果您还没有阅读Swift进化建议书,就去看属性包装器,您可能会想知道为什么我们甚至需要它们。属性包装器只是使Swift看起来像Java,而这显然是不可取的(我在开玩笑,Java是一种很好的语言)。
Swift团队想要向Swift语言添加属性包装器的原因,是为了帮助促进始终应用于属性的通用模式。如果您曾经将某个属性标记为惰性,则可以使用这种模式。 Swift编译器知道如何处理延迟,并且将延迟关键字扩展,使您的属性变为惰性的代码所需的所有代码,都被硬编码到编译器中。
由于可以将更多这些模式应用于属性,因此对所有这些模式进行硬编码是没有意义的。特别是因为属性包装器的目标之一,是允许开发人员以属性包装器的形式提供自己的模式。
让我们聚焦在lazy的关键字。我刚刚提到过,编译器会将您的代码扩展为使您的属性变得lazy的代码。让我们看一下如果没有lazy关键字,而我们不得不自己编写一个lazy属性,那会是什么样子。
这个例子直接取自Swift的开发建议,并为可读性做了一些修改:
struct MyObject { private var _myProperty: Int? var myProperty: Int { get { if let value = _myProperty { return value } let initialValue = 1738 _myProperty = initialValue return initialValue } set { _myProperty = newValue } } }
请注意,此代码比仅编写lazy var myProperty:Int?更冗长。
在编译器中捕获大量这些模式是不理想的,并且它也不是很可扩展。 Swift团队希望允许开发人员可以使用关键字来定义自己的类似于lazy的模式,以帮助他们清理代码并使其代码更具表现力。
请注意,属性包装器不允许开发人员执行其他不可能的事情。 它们仅允许开发人员使用更具表现力的语法来表达模式和意图。 让我们继续看一个例子。
分拆属性包装器我经常使用的属性包装器是Combine的@Published属性包装器。 此属性包装器将应用到的属性转换为发布者,该发布者将在您更改该属性的值时通知订阅者。 此属性包装器的用法如下:
class MyObject { @Published var myProperty = 10 }
很简单,对不对?
要访问创建的发布者,我需要在myProperty上使用$前缀:$ myProperty。 在这种情况下,myProperty属性指向基础值,该基础值是默认值为10的Int类型。 还有一个第二个前缀可以应用于属性包装器,即_,因此是_myProperty。 这是一个私有属性,因此在这种情况下只能从MyObject内部访问,但它告诉我们很多有关属性包装器如何工作的信息。 在上面的MyObject示例中,_myProperty是发布的。 $ myProperty是已发布的发布者,而myProperty是一个Int。 因此,单行代码会导致我们可以访问三种不同的属性。 让我们定义一个自定义属性包装器,找出这三个属性分别是什么,以及它的作用。
@propertyWrapper struct ExampleWrapper{ var wrappedValue: Value }
此属性包装器非常小,根本没有用。 但是,对于我们来说探查属性包装器的结构就足够了。
首先,请注意,ExampleWrapper结构在其定义之前的行上有一个注释:@propertyWrapper。 此注释意味着在它之后定义的结构是属性包装器。 还要注意,ExampleWrapper是通用的。 此值是包装值属性的类型。
属性包装器不必是通用的。 您可以根据需要对wrappedValue的类型进行硬编码。 如果您希望属性包装仅适用于特定类型,则可以对wrappedValue进行硬编码。 或者,如果需要,您可以限制Value的类型。
属性包装器需要wrappedValue属性。 所有属性包装器都必须具有称为wrappedValue的非静态属性。
让我们将此ExampleWrapper发挥作用:
class MyObject { @ExampleWrapper var myProperty = 10 func allVariations() { print(myProperty) //print($myProperty) print(_myProperty) } } let object = MyObject() object.allVariations()
请注意,我已注释掉$ myProperty。 一会儿我将解释原因。
运行此代码时,您会在Xcode的控制台中看到以下内容:
10 ExampleWrapper(wrappedValue: 10)
myProperty仍显示为10.直接访问用属性包装器标记的属性,将打印该属性包装器的wrappedValue属性。 当打印_myProperty时,您将访问属性包装器对象本身。 请注意,_myProperty是MyObject的成员。 您可以键入self._myProperty,即使您从未明确定义_myProperty,Swift也会知道该怎么做。 我之前提到过_myProperty是私有的,因此您不能从MyObject外部访问它,但是它在那里。
原因是Swift编译器将@ExampleWrapper var myProperty = 10在幕后进行了转换:
private var _myProperty: ExampleWrapper= ExampleWrapper(wrappedValue: 10) var myProperty: Int { get { return _myProperty.wrappedValue } set { _myProperty.wrappedValue = newValue } }
我们可以从此示例中学到两件事。首先,您可以看到属性包装器确实不是魔术。它们实际上相对简单。这并没有使它们变得简单或容易,但是一旦您知道将来自单个定义的转换分解为两个单独的定义,则突然变得很容易推论。
_myProperty不是某种神奇的价值。它是由编译器创建的MyObject的真正成员。并且myProperty返回wrappedValue的值,因为它是以这种方式进行硬编码的。不是我们的,而是编译器的。
_myProperty属性称为综合存储属性。这是为包装的值提供存储的属性包装器所在的位置。
那么$ myProperty在哪里?
并非所有的属性包装器都带有$前缀。属性包装的属性的$前缀版本称为预计值。映射值可用于为特定的属性包装器提供特殊的或不同的接口,例如@Published。要将映射值添加到属性包装器,必须在属性包装器定义上实现一个projectedValue属性。
在MyExampleWrapper中,如下所示:
@propertyWrapper struct ExampleWrapper{ var wrappedValue: Value var projectedValue: Value { get { wrappedValue } set { wrappedValue = newValue } } }
这个例子根本没有用,我将在下一节中向您展示一个更有用的例子。 现在,我想向您展示一个属性包装器的剖析。
如果像以前一样使用此属性包装器,Swift将为您生成以下代码:
private var _myProperty: ExampleWrapper= ExampleWrapper(wrappedValue: 10) var myProperty: Int { get { return _myProperty.wrappedValue } set { _myProperty.wrappedValue = newValue } } var $myProperty: Int { get { return _myProperty.projectedValue } set { _myProperty.projectedValue = newValue } }
创建了一个额外的属性,该属性使用私有_myProperty的projectedValue进行获取和设置实现。
由于_myProperty是私有的,因此您的预测值可能会提供对属性包装器的直接访问权限,这是原始属性包装器建议中显示的示例之一。 或者,您可以将完全不同的对象作为属性包装器的投影值公开。@Published属性包装器使用其projectedValue公开发布者。
实施属性包装器我已经向您展示了如何定义一个简单的属性包装器,但是老实说。 这个例子很无聊,有点不好。 在本节中,我们将研究实现一个自定义属性包装器,该包装器将模仿Combine的@Published属性包装器的行为。
让我们先定义一个基础:
@propertyWrapper struct DWPublished{ var wrappedValue: Value }
这定义了包装的属性,该属性包装了任何类型的值。 那挺好的。 此处的目标是实现公开某种发布者的预计价值。 我将为此使用CurrentValueSubject。 每当wrappedValue获得一个新值时,CurrentValueSubject应该向其订阅者发出一个新值。 一个基本的实现可能看起来像这样:
@propertyWrapper class DWPublished{ var wrappedValue: Value { get { subject.value } set { subject.value = newValue } } private let subject: CurrentValueSubject var projectedValue: CurrentValueSubject { get { subject } } init(wrappedValue: Value) { self.subject = CurrentValueSubject(wrappedValue) } }
警告: 此实现是非常基本的,不应用作实际实现@Published的参考。 我确定此代码可能存在错误。 我的目标是帮助您了解属性包装器的工作方式。 并不是要向您展示一个完美的自定义@Published属性包装器。
该代码与您之前看到的代码有很大的不同。 包装的值使用私有对象来实现其获取和设置。 这意味着包装的值始终与对象的当前值同步。
仅指定了projectedValue。 我们不希望该属性包装器的用户将任何东西分配给projectedValue; 它是只读的。
当属性包装器以其最简单的形式初始化时,它会接收其包装的值。 传递给DWPublished的包装值用于设置主题,并将我们应该包装的值作为其初始值。
使用此属性包装器将如下所示:
class MyObject { @DWPublished var myValue = 1 } let obj = MyObject() obj.$myValue.sink(receiveValue: { int in print("received int: \(int)") }) obj.myValue = 2
The printed output for this example would be:
received int: 1 received int: 2
很整洁吧?
由于属性包装器的预计值是CurrentValueSubject,因此它具有我们可以为其分配值的值属性。 如果这样做,属性包装器的wrappedValue也会被更新,因为CurrentValueSubject用于驱动属性包装器的wrappedValue。
obj.$myValue.value = 3 print(obj.myValue) // 3
包裹@Published属性是不可能做到这一点的,因为Apple公开了@Published的projectedValue作为自定义类型Publisher.Publisher而不是CurrentValueSubject。
更复杂的属性包装器可能需要某种配置,例如最大值或最小值。 假设我想扩展我的@DWPublished属性包装器以通过反跳来限制其输出。 我想在MyObject中编写以下代码进行配置:
class MyObject { @DWPublished(debounce: 0.3) var myValue = 1 }
这将以300毫秒的速度消除我发布的值。 我们可以将DWPublished的初始化程序更新为接受此参数,然后重构一下代码:
@propertyWrapper class DWPublished{ var wrappedValue: Value { get { subject.value } set { subject.value = newValue } } private let subject: CurrentValueSubject private let publisher: AnyPublisher var projectedValue: AnyPublisher { get { publisher } } init(wrappedValue: Value, debounce: DispatchQueue.SchedulerTimeType.Stride) { self.subject = CurrentValueSubject(wrappedValue) self.publisher = self.subject .debounce(for: debounce, scheduler: DispatchQueue.global()) .eraseToAnyPublisher() } }
我的属性包装器的初始化程序现在接受去抖动间隔,并使用该间隔创建一个全新的发布者来对我的CurrentValueSubject进行去抖动。 我将此发布者擦除为AnyPublisher,所以我有一种适合我的发布者的类型,而不是Publishers.Debounce