Slide 1

Slide 1 text

游諭 2021/ 02 / 23 @ iOS Taipei Property 透過觀察來理解這要怎麼⽤,並列舉⼀些跟⼀般 property 的異同,看看有什 麼有趣的實作。

Slide 2

Slide 2 text

Agenda ⽬次 1. 來看看 Swift.org 的教學 2. 來看看 @State 3. 來看看 @Published 4. ⽬前網路上看到的有趣實作 5. 進階的 nested Property wrapper 6. 進階的 ThreadSafe

Slide 3

Slide 3 text

來看看 Swift.org 的教學

Slide 4

Slide 4 text

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.

Slide 5

Slide 5 text

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) } } }

Slide 6

Slide 6 text

Property wrapper 的解釋 docs.swift.org/swift-book/LanguageGuide/Properties @propertyWrapper struct TwelveOrLess { … } struct SmallRectangle { @TwelveOrLess var height: Int @TwelveOrLess var width: Int }

Slide 7

Slide 7 text

Where the Compiler is helping From getter / setter to wrapper

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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 }

Slide 11

Slide 11 text

Setting Initial Values for Wrapped Properties Set wrappedValue in `init`

Slide 12

Slide 12 text

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`

Slide 13

Slide 13 text

Projecting a Value From a Property Wrapper The $var part

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Something did not mention computed property(get/set), Property Observers, KeyPath, KVO

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

來看看 SwiftUI 的 @State

Slide 18

Slide 18 text

來看看 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:

Slide 19

Slide 19 text

來看看 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

Slide 20

Slide 20 text

來看看 Combine 的 @Published

Slide 21

Slide 21 text

@Published in a struct 🧐 How to know its enclosing `self` in the wrapper?

Slide 22

Slide 22 text

@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

Slide 23

Slide 23 text

@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.

Slide 24

Slide 24 text

@Published in a struct 🧐 Referencing the enclosing `self` in a wrapper type (SE-0258) @propertyWrapper public struct Observable { private var stored: Value public init(wrappedValue: Value) { self.stored = wrappedValue } public static subscript( instanceSelf observed: OuterSelf, wrapped wrappedKeyPath: ReferenceWritableKeyPath, storage storageKeyPath: ReferenceWritableKeyPath ) -> 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("") } }

Slide 25

Slide 25 text

@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 = Observable(wrappedValue: 17) var myVar: Int { get { Observable[instanceSelf: self, wrapped: \MyClass.myVar, storage: \MyClass._myVar] } set { Observable[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 } } }

Slide 26

Slide 26 text

來看看有趣的實作

Slide 27

Slide 27 text

來看看有趣的實作 ⽬次 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/

Slide 28

Slide 28 text

來看看有趣的實作 https://www.swiftbysundell.com/tips/making-properties-overridable-only-in-debug-builds/ @propertyWrapper struct DebugOverridable { #if DEBUG var wrappedValue: Value #else let wrappedValue: Value #endif } 1. DebugOverrideable

Slide 29

Slide 29 text

來看看有趣的實作 https://olegdreyman.medium.com/keep-private-information-out-of-your-logs-with-swift-bbd2fbcd9a40 @propertyWrapper struct LoggingExcluded: 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

Slide 30

Slide 30 text

來看看有趣的實作 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

Slide 31

Slide 31 text

來看看有趣的實作 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

Slide 32

Slide 32 text

來看看有趣的實作 https://github.com/vapor/fluent-kit Swift ORM (queries, models, and relations) for NoSQL and SQL databases
 
 
 太複雜了不知道怎麼解釋 5. Fluent-kit

Slide 33

Slide 33 text

來看看有趣的實作 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

Slide 34

Slide 34 text

【進階】Nested propertywrapper

Slide 35

Slide 35 text

【進階】Nested propertywrapper 🤔 Nested().$nest1 Nested().$nest2 struct Nested { @State @LoggingExcluded var nest1 = 0 @LoggingExcluded @State var nest2 = 0 }

Slide 36

Slide 36 text

【進階】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 } } 🙅

Slide 37

Slide 37 text

【進階】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

Slide 38

Slide 38 text

【進階】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> Nested().$nest2 ~> Binding @propertyWrapper public struct LoggingExcluding

Slide 39

Slide 39 text

【進階】Atomic Wrapper

Slide 40

Slide 40 text

【進階】Atomic Wrapper @propertyWrapper struct Lock { 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

Slide 41

Slide 41 text

⼀些可以討論的

Slide 42

Slide 42 text

⼀些可以討論的 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

Slide 43

Slide 43 text

結論 Property wrapper

Slide 44

Slide 44 text

結論 Property wrapper 好奇 3 問 Property wrapper 帶給 Developer 有什麼不同?
 
 Property wrapper 有沒有 Anti-Pattern? Property wrapper 有沒有必要使⽤?