Share
Embed
Property wrapper in Swift
透過觀察來理解 Property wrapper 要怎麼用,並列舉一些跟一般 property 的異同,看看有什麼有趣的實作。
Presentation on 2021 / 02 / 23 @ iOS Taipei
Tsungyu Yu
February 23, 2021
Transcript
44 slides
-
游諭 2021/ 02 / 23 @ iOS Taipei Property 透過觀察來理解這要怎麼⽤,並列舉⼀些跟⼀般
property 的異同,看看有什 麼有趣的實作。 -
Agenda ⽬次 1. 來看看 Swift.org 的教學 2. 來看看 @State 3.
來看看 @Published 4. ⽬前網路上看到的有趣實作 5. 進階的 nested Property wrapper 6. 進階的 ThreadSafe -
Property wrapper 的解釋 docs.swift.org/swift-book/LanguageGuide/Properties 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. -
Property wrapper 的解釋 docs.swift.org/swift-book/LanguageGuide/Properties @propertyWrapper struct TwelveOrLess { private var
number: Int init() { self.number = 0 } var wrappedValue: Int { get { return number } set { number = min(newValue, 12) } } } -
Property wrapper 的解釋 docs.swift.org/swift-book/LanguageGuide/Properties @propertyWrapper struct TwelveOrLess { … }
struct SmallRectangle { @TwelveOrLess var height: Int @TwelveOrLess var width: Int } -
Where the Compiler is helping From getter / setter to
wrapper When you apply a wrapper to a property, the compiler synthesizes code that provides storage for the wrapper and code that provides access to the property through the wrapper. (The property wrapper is responsible for storing the wrapped value, so there’s no synthesized code for that.) You could write code that uses the behavior of a property wrapper, without taking advantage of the special attribute syntax. -
For example, here’s a version of SmallRectangle from the previous
code listing that wraps its properties in the TwelveOrLess structure explicitly, instead of writing @TwelveOrLess as an attribute: struct SmallRectangle { private var _height = TwelveOrLess() private var _width = TwelveOrLess() var height: Int { get { return _height.wrappedValue } set { _height.wrappedValue = newValue } } var width: Int { get { return _width.wrappedValue } set { _width.wrappedValue = newValue } } } Where the Compiler is helping From getter / setter to wrapper -
struct SmallRectangle { private var _height = TwelveOrLess() private var
_width = TwelveOrLess() var height: Int { get { return _height.wrappedValue } set { _height.wrappedValue = newValue } } var width: Int { get { return _width.wrappedValue } set { _width.wrappedValue = newValue } } } struct SmallRectangle { @TwelveOrLess var height: Int @TwelveOrLess var width: Int } -
struct ZeroRectangle { @SmallNumber var height: Int @SmallNumber var width:
Int = 1 @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int @SmallNumber(maximum: 9) var width: Int = 2 } @propertyWrapper struct SmallNumber { private var maximum: Int private var number: Int var wrappedValue: Int { get { return number } set { number = min(newValue, maximum) } } init() { maximum = 12 number = 0 } init(wrappedValue: Int) { maximum = 12 number = min(wrappedValue, maximum) } init(wrappedValue: Int, maximum: Int) { self.maximum = maximum number = min(wrappedValue, maximum) } } Setting Initial Values for Wrapped Properties Set wrappedValue in `init` -
struct SomeStructure { @SmallNumber var someNumber: Int } var someStructure
= SomeStructure() someStructure.someNumber = 4 someStructure.$someNumber @propertyWrapper struct SmallNumber { private var number: Int var projectedValue: Bool init() { self.number = 0 self.projectedValue = false } var wrappedValue: Int { get { return number } set { if newValue > 12 { number = 12 projectedValue = true } else { number = newValue projectedValue = false } } } } Projecting a Value From a Property Wrapper The $var part -
Something did not mention computed property(get/set), Property Observers, KeyPath, KVO
•(get/set) 🙅 •(willSet/didSet) 🙆 •KeyPath both works on wrappedValue / projectedValue •KVO OK, Wrapper 可以與 @objc dynamic ⼀起⽤ •_var 是 private 宣告,$Project (⽬前)不可以⽤ extension // Nested 補充 Property wrapper cannot be applied to a computed property .map(\.someNumber) .map(\.$someNumber) @SmallNumber @objc dynamic var someNumber -
來看看 SwiftUI 的 @State A property wrapper type that can
read and write a value managed by SwiftUI. SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view. A State instance isn’t the value itself; it’s a means of reading and writing the value. To access a state’s underlying value, use its variable name, which returns the wrappedValue property value. You should only access a state property from inside the view’s body, or from methods called by it. For this reason, declare your state properties as private, to prevent clients of your view from accessing them. It is safe to mutate state properties from any thread. To pass a state property to another view in the view hierarchy, use the variable name with the $ prefix operator. This retrieves a binding of the state property from its projectedValue property. For example, in the following code example PlayerView passes its state property isPlaying to PlayButton using $isPlaying: -
來看看 SwiftUI 的 @State Federico 做了⼀個 FSState https://fivestars.blog/swiftui/lets-build-state.html 1. Propertywrapper
2. ContentView is a struct 3. SwiftUI auto update 4. $text is a Binding struct ContentView: View { @FSState var text = "" var body: some View { VStack { TextField("Write something", text: $text) } } } Cannot assign to property: 'self' is immutable -
@Published in a struct 🧐 How to know its enclosing
`self` in the wrapper? -
@Published in a struct 🧐 How to know its enclosing
`self` in the wrapper? import Combine struct WrapperOwner { @Published var i = 0 } 'wrappedValue' is unavailable: @Published is only available on properties of classes -
@Published in a struct 🧐 Referencing the enclosing `self` in
a wrapper type (SE-0258) SE-0258 Property-wrappers 之 Referencing the enclosing 'self' in a wrapper type https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type Instead of a wrappedValue property, a property wrapper type could provide a static subscript(instanceSelf:wrapped:storage:)that receives self as a parameter, along with key paths referencing the original wrapped property and the backing storage property. -
@Published in a struct 🧐 Referencing the enclosing `self` in
a wrapper type (SE-0258) @propertyWrapper public struct Observable<Value> { private var stored: Value public init(wrappedValue: Value) { self.stored = wrappedValue } public static subscript<OuterSelf: Observed>( instanceSelf observed: OuterSelf, wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>, storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self> ) -> Value { get { observed[keyPath: storageKeyPath].stored } set { let oldValue = observed[keyPath: storageKeyPath].stored if newValue != oldValue { observed.broadcastValueWillChange(newValue: newValue) } observed[keyPath: storageKeyPath].stored = newValue } } } @available(*, unavailable, message: " ... ") var wrappedValue: Value { get { fatalError("") } set { fatalError("") } } -
@Published in a struct 🧐 Referencing the enclosing `self` in
a wrapper type (SE-0258) class MyClass: Superclass { @Observable public var myVar: Int = 17 // desugars to... private var _myVar: Observable<Int> = Observable(wrappedValue: 17) var myVar: Int { get { Observable<Int>[instanceSelf: self, wrapped: \MyClass.myVar, storage: \MyClass._myVar] } set { Observable<Int>[instanceSelf: self, wrapped: \MyClass.myVar, storage: \MyClass._myVar] = newValue } } } struct SmallRectangle { private var _height = TwelveOrLess() var height: Int { get { return _height.wrappedValue } set { _height.wrappedValue = newValue } } } -
來看看有趣的實作 ⽬次 1. DebugOverrideable https://www.swiftbysundell.com/tips/making-properties-overridable-only-in-debug-builds/ 2. LoggingExcluded https://olegdreyman.medium.com/keep-private-information-out-of-your-logs-with-swift-bbd2fbcd9a40
3. SecureAppStorage https://gist.github.com/pauljohanneskraft/4652fbeae67a2206ad6b4296675e9bb5 4. BetterCodable https://github.com/marksands/BetterCodable 5. Fluent-kit https://github.com/vapor/fluent-kit 6. Proxy https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ -
來看看有趣的實作 https://www.swiftbysundell.com/tips/making-properties-overridable-only-in-debug-builds/ @propertyWrapper struct DebugOverridable<Value> { #if DEBUG var wrappedValue:
Value #else let wrappedValue: Value #endif } 1. DebugOverrideable -
來看看有趣的實作 https://olegdreyman.medium.com/keep-private-information-out-of-your-logs-with-swift-bbd2fbcd9a40 @propertyWrapper struct LoggingExcluded<Value>: CustomStringConvertible, CustomDebugStringConvertible, CustomLeafReflectable { var
wrappedValue: Value init(wrappedValue: Value) { self.wrappedValue = wrappedValue } var description: String { return "--redacted--" } var debugDescription: String { return "--redacted--" } var customMirror: Mirror { return Mirror(reflecting: "--redacted--") } } 2. LoggingExcluded -
來看看有趣的實作 https://gist.github.com/pauljohanneskraft/4652fbeae67a2206ad6b4296675e9bb5 @propertyWrapper struct SecureAppStorage { var item: KeychainItem init(_
account: String, service: String = Bundle.main.bundleIdentifier!) { self.item = .init(service: service, account: account) } public var wrappedValue: String? { get { item.get() } nonmutating set { item.set(newValue) } } } 3. SecureAppStorage -
來看看有趣的實作 https://github.com/marksands/BetterCodable Level up your Codable structs through property wrappers.
The goal of these property wrappers is to avoid implementing a custom init(from decoder: Decoder) throws and suffer through boilerplate. struct Response: Codable { @LossyArray var values: [Int] } let json = #"{ "values": [1, 2, null, 4, 5, null] }"#.data(using: .utf8)! let result = try JSONDecoder().decode(Response.self, from: json) print(result) // [1, 2, 4, 5] 4. BetterCodable -
來看看有趣的實作 https://github.com/vapor/fluent-kit Swift ORM (queries, models, and relations) for NoSQL
and SQL databases 太複雜了不知道怎麼解釋 5. Fluent-kit -
來看看有趣的實作 https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ class HeaderView: UIView { @Proxy(\.titleLabel.text) var title: String?
@Proxy(\.imageView.image) var image: UIImage? private let titleLabel = UILabel() private let imageView = UIImageView() } 6. Proxy -
【進階】Nested propertywrapper 🤔 Nested().$nest1 Nested().$nest2 struct Nested { @State @LoggingExcluded
var nest1 = 0 @LoggingExcluded @State var nest2 = 0 } -
【進階】Nested propertywrapper struct Nested { @State @LoggingExcluded var nest1 =
0 @LoggingExcluded @State var nest2 = 0 } public protocol Projected { associatedtype ProjectedValue var projectedValue: ProjectedValue { get } } extension LoggingExcluded: Projected where Value: Projected { public typealias ProjectedValue = Value.ProjectedValue public var projectedValue: Value.ProjectedValue { wrappedValue.projectedValue } } 🙅 -
【進階】Nested propertywrapper struct Nested { @State @LoggingExcluded var nest1 =
0 @LoggingExcluded @State var nest2 = 0 } public protocol Projected { associatedtype ProjectedValue var projectedValue: ProjectedValue { get } } @propertyWrapper public struct LoggingExcluding<Value: Projected> -
【進階】Nested propertywrapper struct Nested { @State @LoggingExcluding var nest1 =
0 @LoggingExcluding @State var nest2 = 0 } public protocol Projected { associatedtype ProjectedValue var projectedValue: ProjectedValue { get } } public var projectedValue: Value.ProjectedValue { wrappedValue.projectedValue } Nested().$nest1 ~> Binding<LoggingExcluded<Int>> Nested().$nest2 ~> Binding<Int> @propertyWrapper public struct LoggingExcluding<Value: Projected> -
【進階】Atomic Wrapper @propertyWrapper struct Lock<Value> { private var inner: LockInner
init(wrappedValue: Value) { inner = LockInner(wrappedValue) } var wrappedValue: Value { get { return inner.value } nonmutating _modify { inner.lock.lock() defer { inner.lock.unlock() } yield &inner.value } } private class LockInner { let lock = NSLock() var value: Value init(_ value: Value) { self.value = value } } } 網路上有不少實作,不過這個是我經過單元測試實測有⽤ 請看⽰範 ThreadSafe/Lock.swift -
⼀些可以討論的 See also 1. apple/swift 的編譯器討論 (C++) 2. Owner 繼承、lazy、weak、unowned
(劇透:🙃) 3. Swift 5.4 的 Local Property wrapper 4. apple/swift 的編寫案例 (很多沒有討論到的) https://github.com/apple/swift/blob/main/test/decl/var/property_wrappers.swift 5. SwiftUI 已經有的 Property wrapper https://www.hackingwithswift.com/quick-start/swiftui/all-swiftui-property-wrappers-explained-and-compared -
結論 Property wrapper 好奇 3 問 Property wrapper 帶給 Developer
有什麼不同? Property wrapper 有沒有 Anti-Pattern? Property wrapper 有沒有必要使⽤?