Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Property wrapper in Swift

F52f4adb291c5b44bd8ad1433dfedaaf?s=47 Tsungyu Yu
February 23, 2021

Property wrapper in Swift

透過觀察來理解 Property wrapper 要怎麼用,並列舉一些跟一般 property 的異同,看看有什麼有趣的實作。

Presentation on 2021 / 02 / 23 @ iOS Taipei

F52f4adb291c5b44bd8ad1433dfedaaf?s=128

Tsungyu Yu

February 23, 2021
Tweet

Transcript

  1. 游諭 2021/ 02 / 23 @ iOS Taipei Property 透過觀察來理解這要怎麼⽤,並列舉⼀些跟⼀般

    property 的異同,看看有什 麼有趣的實作。
  2. Agenda ⽬次 1. 來看看 Swift.org 的教學 2. 來看看 @State 3.

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

  4. 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.
  5. 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) } } }
  6. Property wrapper 的解釋 docs.swift.org/swift-book/LanguageGuide/Properties @propertyWrapper struct TwelveOrLess { … }

    struct SmallRectangle { @TwelveOrLess var height: Int @TwelveOrLess var width: Int }
  7. Where the Compiler is helping From getter / setter to

    wrapper
  8. 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.
  9. 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
  10. 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 }
  11. Setting Initial Values for Wrapped Properties Set wrappedValue in `init`

  12. 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`
  13. Projecting a Value From a Property Wrapper The $var part

  14. 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
  15. Something did not mention computed property(get/set), Property Observers, KeyPath, KVO

  16. 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
  17. 來看看 SwiftUI 的 @State

  18. 來看看 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:
  19. 來看看 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
  20. 來看看 Combine 的 @Published

  21. @Published in a struct 🧐 How to know its enclosing

    `self` in the wrapper?
  22. @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
  23. @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.
  24. @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("") } }
  25. @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 } } }
  26. 來看看有趣的實作

  27. 來看看有趣的實作 ⽬次 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/
  28. 來看看有趣的實作 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
  29. 來看看有趣的實作 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
  30. 來看看有趣的實作 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
  31. 來看看有趣的實作 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
  32. 來看看有趣的實作 https://github.com/vapor/fluent-kit Swift ORM (queries, models, and relations) for NoSQL

    and SQL databases
 
 
 太複雜了不知道怎麼解釋 5. Fluent-kit
  33. 來看看有趣的實作 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
  34. 【進階】Nested propertywrapper

  35. 【進階】Nested propertywrapper 🤔 Nested().$nest1 Nested().$nest2 struct Nested { @State @LoggingExcluded

    var nest1 = 0 @LoggingExcluded @State var nest2 = 0 }
  36. 【進階】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 } } 🙅
  37. 【進階】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>
  38. 【進階】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>
  39. 【進階】Atomic Wrapper

  40. 【進階】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
  41. ⼀些可以討論的

  42. ⼀些可以討論的 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
  43. 結論 Property wrapper

  44. 結論 Property wrapper 好奇 3 問 Property wrapper 帶給 Developer

    有什麼不同?
 
 Property wrapper 有沒有 Anti-Pattern? Property wrapper 有沒有必要使⽤?