propertyWrapper相關(guān)

簡(jiǎn)介

WWDC2019 發(fā)布了 SwiftUI 和 Swift5.1,我們看到很多全新帶 @ 的屬性例如 @State、@Binding、@EnvironmentObject,這些其實(shí)都依賴于 Property Wrappers。

動(dòng)機(jī)

Swift 語(yǔ)言為屬性提供了一些關(guān)鍵字,比如 layz 它推遲初始化其默認(rèn)值,直到第一次訪問(wèn)才進(jìn)行初始化,我們可以自己嘗試實(shí)現(xiàn)。

struct Too {
  // lazy var too = 1
  private var _too: Int?
  var too: Int {
    mutating get {
      if let value = _too { return value }
      let initialValue = 1
      _too = initialValue
      return initialValue
    }
    set {
      _too = newValue
    }
  }
}

Swift 語(yǔ)言讓我們能夠使用 lazy 很方便的讓屬性獲得推遲初始化的功能,但是把 lazy 構(gòu)建到語(yǔ)言層面也有一些缺點(diǎn)。會(huì)使語(yǔ)言和編譯器變的更復(fù)雜,也不靈活。惰性初始化還有很多有意義的變體,但是語(yǔ)言層面不會(huì)全部支持。

而且我除了懶加載以外我們可能還需要很多不同屬性模式,語(yǔ)言層面也不會(huì)全部支持,我自己在每個(gè)屬性的 set, get 方法里面去實(shí)現(xiàn)又太麻煩,所以我們需要一個(gè)模板,能讓我們快速的實(shí)現(xiàn)我們自己的屬性關(guān)鍵字,然后放到屬性前面。所以 Property Wrappers 就誕生了。

我們現(xiàn)在可以通過(guò) Property Wrappers 來(lái)自己實(shí)現(xiàn)一個(gè) lazy 關(guān)鍵字。

@propertyWrapper
enum Lazy<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)

  init(wrappedValue: @autoclosure @escaping () -> Value) {
    self = .uninitialized(wrappedValue)
  }

  var wrappedValue: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      self = .initialized(newValue)
    }
  }
}

struct Too {
  @Lazy var too = 1
}

屬性包裝器

屬性包裝器就是在管理屬性如何存儲(chǔ)和定義屬性的代碼之間添加了一個(gè)分隔層。一次性定義好屬性的管理代碼,我們就可以在多個(gè)屬性上進(jìn)行復(fù)用。

定義一個(gè)屬性包裝器,你需要?jiǎng)?chuàng)建一個(gè)定義 wrappedValue 屬性的結(jié)構(gòu)體、枚舉或者類。

@propertyWrapper
struct Limit {
    private var value: Double
    private var range: ClosedRange<Double>

    init() {
        value = 0
        self.range = 0...1
    }

    var wrappedValue: Double {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }
}

struct Movie {
    @Limit var progress: Double
}


var movie = Movie()
movie.progress = 2
print(movie.progress)
// 打印 1.0

movie.progress = -1
print(movie.progress)
// 打印 0.0

movie.progress = 0.5
print(movie.progress)
// 打印 0.5

當(dāng)你把一個(gè)包裝器應(yīng)用到一個(gè)屬性上時(shí),編譯器將合成提供包裝器存儲(chǔ)空間和通過(guò)包裝器訪問(wèn)屬性的代碼。等同于下面的代碼。

struct Movie {
    private var _progress = Limit()
    var progress: Double {
        get { return _progress.wrappedValue }
        set { _progress.wrappedValue = newValue }
    }
}

設(shè)置被包裝屬性的初始值

我們給上面的 Limit 屬性包裝器再添加兩個(gè)構(gòu)造器.

@propertyWrapper
struct Limit {
    private var value: Double
    private var range: ClosedRange<Double>

    var wrappedValue: Double {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }

    init() {
        value = 0
        range = 0...1
    }

    init(wrappedValue: Double) {
        range = 0...1
        value = min(max(range.lowerBound, wrappedValue), range.upperBound)
    }

    init(wrappedValue: Double, range: ClosedRange<Double>) {
        self.range = range
        value = min(max(range.lowerBound, wrappedValue), range.upperBound)
    }
}

當(dāng)你把包裝器應(yīng)用于屬性且沒(méi)有設(shè)定初始值時(shí),Swift 使用 init() 構(gòu)造器來(lái)設(shè)置包裝器。舉個(gè)例子:

struct Movie {
    @Limit var progress: Double
}

var movie = Movie()
print(movie.progress)
// 打印 0.0

當(dāng)你為屬性指定初始值時(shí),Swift 使用 init(wrappedValue:) 構(gòu)造器來(lái)設(shè)置包裝器。舉個(gè)例子:

struct Movie {
    @Limit var progress = 0.6
}

var movie = Movie()
print(movie.progress)
// 打印 0.6

這里當(dāng)你設(shè)置 = 0.6 時(shí), 調(diào)用 Limit(wrappedValue: 0.6) 來(lái)創(chuàng)建包裝 progress 的實(shí)例。

當(dāng)你在自定義特性后面把實(shí)參寫(xiě)在括號(hào)里時(shí),Swift 使用接受這些實(shí)參的構(gòu)造器來(lái)設(shè)置包裝器。

struct Movie {
    @Limit(wrappedValue: 0.6, range: 0...1) var progress 
}

var movie = Movie()
print(movie.progress)
// 打印 0.6

這里等于調(diào)用了 init(wrappedValue: 0.6, range: 0...1) 來(lái)創(chuàng)建實(shí)例。

當(dāng)包含屬性包裝器實(shí)參時(shí),你也可以使用賦值來(lái)指定初始值。Swift 將賦值視為 wrappedValue 參數(shù),且使用接受被包含的實(shí)參的構(gòu)造器。舉個(gè)例子:

struct Movie {
    @Limit(range: 0...1) var progress = 0.6
}

var movie = Movie()
print(movie.progress)
// 打印 0.6

這里等于調(diào)用了 init(wrappedValue: 0.6, range: 0...1) 來(lái)創(chuàng)建實(shí)例。

ProjectedValue

除了被包裝值,屬性包裝器可以通過(guò)定義被呈現(xiàn)值暴露出其他功能。屬性包裝器可以返回任何類型的值作為它的被呈現(xiàn)值。除了以貨幣符號(hào)($)開(kāi)頭,被呈現(xiàn)值的名稱和被包裝值是一樣的。

@propertyWrapper
struct SmallNumber {
    private var number: Int
    var projectedValue: Bool
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// 打印 "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// 打印 "true"

寫(xiě)下 someStructure.someNumber 即可訪問(wèn)包裝器的被呈現(xiàn)值。在存儲(chǔ)一個(gè)比較小的數(shù)值時(shí),如 4 ,someStructure.someNumber 的值為 false。但是,在嘗試存儲(chǔ)一個(gè)較大的數(shù)值時(shí),如 55 ,被呈現(xiàn)值變?yōu)?true。

@State

來(lái)看一看 SwiftUI 中 @State 中的實(shí)現(xiàn) public 的部分只有幾個(gè)初始化方法和 property wrapper 的標(biāo)準(zhǔn)的 value:

struct State<Value> : DynamicProperty {
    init(wrappedValue value: Value)
    init(initialValue value: Value)
    var wrappedValue: Value { get nonmutating set }
    var projectedValue: Binding<Value> { get }
}

可以看到有兩個(gè)初始化方法, 從文檔上面沒(méi)有看出兩者有什么區(qū)別, 使用 init(wrappedValue value: Value) 時(shí)候, 蘋(píng)果建議我門(mén)不要直接調(diào)用初始化方法,而是使用聲明屬性賦值的方式,例如:

@State private var isPlaying: Bool = false

我們可以通過(guò) dump State 的值,很容易知道它的幾個(gè)私有變量,繼而猜測(cè)其內(nèi)部結(jié)構(gòu).

struct ContentView: View {
    @State var flag = false

    init() {
        dump(self._flag)
    }

    var body: some View {
        get {
            Button("flag = \(flag)" as String, action: { self.flag = true; dump(self._flag) })
        }
        
    }
}

我們可以大致猜測(cè)一下 @State 結(jié)構(gòu):

@propertyWrapper
struct MyState<Value>:DynamicProperty{
    private var _value:Value
    private var _location:StoredLocation<Value>?

    init(wrappedValue:Value){
        self._value = wrappedValue
        self._location = StoredLocation(value: wrappedValue)
    }

    var wrappedValue:Value{
        get{ _location?._value.pointee ?? _value}
        nonmutating set { 
            _location?._value.pointee = newValue
            update()
        }
    }

    var projectedValue:Binding<Value>{
        Binding<Value>(
            get:{self.wrappedValue},
            set:{self._location?._value.pointee = $0}
        )
    }

    func update() {
        print("重繪視圖")
    }
}


class StoredLocation<Value>{
    let _value = UnsafeMutablePointer<Value>.allocate(capacity: 1)
    init(value:Value){
        self._value.pointee = value
    }
}

這段代碼并不能和視圖建立依賴。但是我們可以和使用 @State 一樣來(lái)使用 @MyState,同樣支持綁定、修改,除了視圖不會(huì)自動(dòng)刷新。

這里你可能會(huì)有個(gè)疑問(wèn)為啥單獨(dú)用了一個(gè) class 來(lái)存儲(chǔ) value,不直接就用 var _value 屬性.

SwiftUI 里面, 我們會(huì)經(jīng)常在 body 里對(duì) @State 修飾屬性的值進(jìn)行更改, 從而觸發(fā)界面 View 的刷新.然而在 Struct 中本身是個(gè)常量要更改屬性屬性需要 mutating 關(guān)鍵字修飾,例如:

struct ContentView: View {
    var number: Int = 1
    var body: some View {
        mutating get {
            Button {
               number += 1
            } label: {
                Text("Text")
            }
        }
    }
}

但是在 SwiftUI body 使用 mutating 編譯報(bào)錯(cuò) Type 'ContentView' does not conform to protocol 'View'

struct ContentView: View {
    var number: Int = 1
    var body: some View {
        mutating get {
            Text("ContentView")
        }
    }
}

View body 沒(méi)法修改那么只能去修改屬性 set 方法使用 nonmutating 告訴編譯器不會(huì)修改內(nèi)部的值.例如:

struct ContentView: View {
    private var _number: Int = 0
    var number: Int {
        get {
            return _number
        }
        nonmutating set {
            print("存儲(chǔ)新值")
        }
    }
    var body: some View {
        get {
            Button {
               number += 1
            } label: {
                Text("Text")
            }
        }
    }
}

然后再把 _number 換成我們 StoredLocation 里面又一個(gè)存儲(chǔ) value 的指針.
以上就是 @State 關(guān)于 propertyWrapper 使用.

struct Demo {
    @State var number: Int

    init(number: Int) {
        self.number = number + 1
    }
}

上述代碼會(huì)有編譯錯(cuò)誤 Variable 'self.number' used before being initialized, 開(kāi)始你會(huì)感覺(jué)很奇怪. 但仔細(xì)一想便知道了, @State 的聲明,會(huì)在當(dāng)前 View 中帶來(lái)一個(gè)自動(dòng)生成的私有存儲(chǔ)屬性.

truct Demo {
    private var _number: State<Int>
    var number : Int {
        get {
            _number.wrappedValue
        }
        nonmutating set {
            _number.wrappedValue = newValue
        }
    }

    init(number: Int) {
        self.number = number
    }
}

init 里面調(diào)用 self.number 的時(shí)候,底層 _number 是沒(méi)有完成初始化的.

Examples

屬性需要用 UserDefaults 進(jìn)行存儲(chǔ)的.

@propertyWrapper
struct UserDefault<T> {
  let key: String
  let defaultValue: T
  
  var wrappedValue: T {
    get {
      return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
}

enum GlobalSettings {
  @UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
  static var isFooFeatureEnabled: Bool
  
  @UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
  static var isBarFeatureEnabled: Bool
}

屬性的存儲(chǔ)值限定到某個(gè)特定范圍內(nèi).

@propertyWrapper
struct Clamping<V: Comparable> {
  var value: V
  let min: V
  let max: V

  init(wrappedValue: V, min: V, max: V) {
    value = wrappedValue
    self.min = min
    self.max = max
    assert(value >= min && value <= max)
  }

  var wrappedValue: V {
    get { return value }
    set {
      if newValue < min {
        value = min
      } else if newValue > max {
        value = max
      } else {
        value = newValue
      }
    }
  }
}

struct Color {
  @Clamping(min: 0, max: 255) var red: Int = 127
  @Clamping(min: 0, max: 255) var green: Int = 127
  @Clamping(min: 0, max: 255) var blue: Int = 127
}

組合

屬性包裝器還可以組合使用,比如把之前 @Lazy 和 @Clamping 給一個(gè)屬性那這個(gè)屬性就能獲得兩種效果.

struct Demo {
    @Lazy @Clamping(min: 0, max: 255) var red: Int = 127
// 相當(dāng)于下面代碼
//    private var _red: Lazy<Clamping<Int>> = Lazy(wrappedValue: Clamping<Int>(wrappedValue: 127, min: 0, max: 255))
//    var red: Int {
//        mutating get {
//           return _red.wrappedValue.wrappedValue
//        }
//        set {
//            _red.wrappedValue.wrappedValue = newValue
//        }
//    }
}

參考

the swift programming language

SE-0258: Property Wrappers

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容