Swift和SwiftUI中Property warppers的使用

閱讀時(shí)長35分鐘罩句,Swift5.0開始支持Property warppers屬性包裝器,顧名思義就是對屬性進(jìn)行包裝李皇,給屬性附加一段邏輯祟印,同時(shí)對這段邏輯操作進(jìn)行了封裝肴沫,在背后透明的運(yùn)行,這樣的好處是能大大的增加代碼的重用率蕴忆。目前AppleSwiftUI框架就是用了很多屬性包裝器如常用的@State颤芬,@Binding@Appstroe套鹅,@StateObject站蝠,@ObserveObject和@EnvironmentObject等來封裝屬性邏輯并進(jìn)行監(jiān)聽,當(dāng)屬性發(fā)生改變時(shí)同步的刷新UI芋哭,通過本文你將會(huì)學(xué)習(xí)到一下內(nèi)容:

  1. 了解SwiftUI原生Property warppers屬性包裝器的實(shí)現(xiàn)邏輯沉衣。
  2. 理解Property warppers的本質(zhì)。
  3. Demo自定義實(shí)現(xiàn)Property warppers减牺。

初步認(rèn)識Property warppers

在上面我們已經(jīng)反復(fù)提到property warppers屬性包裝器是一段附加在屬性上的邏輯豌习,這樣的好處是能極大的利用代碼的復(fù)用率存谎,下面我們通過一個(gè)例子來感性的認(rèn)識下。

struct ContentView: View {
    @AppStorage("username") var username: String = "Anonymous"
    var body: some View {
        VStack {
            Text("Welcome, \(username)!")
            Button("Log in") {
                username = "@twostraws"
            }
        }
    }
}

上面的代碼中使用了一個(gè)SwiftUI中的@AppStorage屬性包裝器肥隆,利用Button來修改屬性userName的值既荚,同時(shí)@AppStorage附加在屬性上的邏輯會(huì)將"@twostraws"這個(gè)value通過key"username"保存到UserDefaults中,并監(jiān)聽這個(gè)value值的改變栋艳,當(dāng)value值改變時(shí)恰聘,SwiftUI會(huì)通知所有持有這個(gè)屬性值的view進(jìn)行刷新,故當(dāng)點(diǎn)擊Button時(shí)吸占,Text的顯示內(nèi)容會(huì)從"Welcome, Anonymous !"變?yōu)?strong>"Welcome, @twostraws !"晴叨。

假如沒有屬性包裝器,要實(shí)現(xiàn)上面的代碼矾屯,大致的邏輯則是需要重寫uesrName屬性的set方法兼蕊,并在set方法里發(fā)出改變的通知,view在收到通知時(shí)進(jìn)行刷新件蚕,但是一旦要多個(gè)屬性需要有上面相同的實(shí)現(xiàn)時(shí)孙技,就要對每個(gè)屬性都要進(jìn)行set方法的重寫,并發(fā)出通知排作,這樣會(huì)導(dǎo)致很多重復(fù)的邏輯代碼牵啦,而通過Property warppers屬性包裝器則只需在屬性前面加上關(guān)鍵字就能對同樣的代碼在背后進(jìn)行透明的封裝,大大提高代碼的重復(fù)利用率妄痪。

自定義property warppers

主要步驟

  • 自定義一個(gè)struct結(jié)構(gòu)體或者class類哈雏,并在前面用上@propertyWrapper關(guān)鍵字。
  • 必須有名為wrappedValue的屬性拌夏,用來告訴swift被附加邏輯包裝后的值僧著。
@propertyWrapper struct Capitalized {
    var wrappedValue: String {
        didSet { wrappedValue = wrappedValue.capitalized }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.capitalized
    }
}

代碼解讀:

這里我們想自定義一個(gè)將所有String值進(jìn)行首字符大寫的屬性包裝器履因,用struct結(jié)構(gòu)體定義了一個(gè)名為Capitalized的屬性包裝器障簿,定義了一個(gè)init初始化方法和wrappedValue屬性,并對set方法添加了屬性觀察栅迄,初始化方法中設(shè)置wrappedValue為首字符大寫站故,當(dāng)設(shè)置wrappedValue屬性值時(shí)同樣也進(jìn)行首字符大寫操作。

使用:

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

利用@Capitalized在標(biāo)記firstName屬性毅舆,這樣涉及firstNamelastName屬性的讀寫操作其實(shí)是操作對應(yīng)的屬性包裝器中的wrappedValue屬性西篓,這樣就實(shí)現(xiàn)了在給firstNamelastName屬性賦值后,背后的邏輯已經(jīng)進(jìn)行了首字母大寫的操作憋活。

// John Appleseed
var user = User(firstName: "john", lastName: "appleseed")
// John Sundell
user.lastName = "sundell"

注意:

  • 當(dāng)屬性包裝器的init方法定義為init(wrappedValue:)時(shí)岂津,則可以直接給包裝的屬性賦默認(rèn)值,例如@Capitalized var name = "Untitled document"悦即。
  • Swfit中的屬性觀察在所有屬性完成初始化之前是不會(huì)觸發(fā)的吮成,所以需要顯示的定義初始化方法橱乱,以便讓屬性初始化完后能觸發(fā)屬性。

觀察屬性包裝器由于其定義是structclass粱甫,所有也能擁有自己的屬性泳叠,能實(shí)現(xiàn)依賴注入,可以實(shí)現(xiàn)復(fù)雜的封裝邏輯茶宵。

@propertyWrapper struct UserDefaultsBacked<Value> {
    let key: String // 內(nèi)部的屬性
    var storage: UserDefaults = .standard // 內(nèi)部的屬性
    var wrappedValue: Value? { // 必須要實(shí)現(xiàn)的屬性危纫,注意這里是可選項(xiàng)
        get { storage.value(forKey: key) as? Value }
        set { storage.setValue(newValue, forKey: key) }
    }
}

由于Swift中結(jié)構(gòu)體是有一個(gè)默認(rèn)的初始化器,所以初始化時(shí)只需初始化key這個(gè)屬性乌庶。

struct SettingsViewModel {
    @UserDefaultsBacked<String>(key: "mark-as-read")  var autoMarkMessagesAsRead
}
    var setModelOne = SettingsViewModel() // key:mark-as-read
    var setModelTwo = SettingsViewModel(autoMarkMessagesAsRead: UserDefaultsBacked(key: "mark-as-blue")) // key:mark-as-blue
    // 由于沒有init(wrappedValue:)初始化方法
    // var setModelThree = SettingsViewModel(autoMarkMessagesAsRead:"Mamba")這樣的是不行的
    setModelOne.autoMarkMessagesAsRead = "8888"
    setModelTwo.autoMarkMessagesAsRead = "9999"
    print(setModelOne.autoMarkMessagesAsRead) //Optional("8888")
    print(setModelTwo.autoMarkMessagesAsRead) //Optional("9999")

代碼解讀:

  • 通多對UserDefaultsBacked屬性包裝器進(jìn)行泛型定義來存儲不同類型的值种蝶。
  • UserDefaultsBacked內(nèi)定義了keystorage屬性,其中storage可以讓屬性包裝器實(shí)現(xiàn)依賴注入瞒大。

雖然上面autoMarkMessagesAsReadnumberOfSearchResultsPerPage二個(gè)屬性都是非可選的蛤吓,但是經(jīng)過@UserDefaultsBacked進(jìn)行屬性包裝包裝后,其值其實(shí)是可選的糠赦,因?yàn)楸澈蟮?strong>wrappedValue是可選的会傲,當(dāng)對應(yīng)的key沒值時(shí)取到的是nil,在使用的時(shí)候還需要進(jìn)行解包的操作拙泽,這樣會(huì)很麻煩淌山,解決辦法是可以返回一個(gè)定義defaultValue的屬性值,當(dāng)key取到的值為時(shí)返回這個(gè)默認(rèn)值顾瞻。

@propertyWrapper struct UserDefaultsBacked<Value> {
    var wrappedValue: Value { // 非可選項(xiàng)
        get {
            let value = storage.value(forKey: key) as? Value // 進(jìn)行可選項(xiàng)綁定判斷泼疑,為nil則但會(huì)defaultValue
            return value ?? defaultValue
        }
        set {
            storage.setValue(newValue, forKey: key)
        }
    }
    private let key: String
    private let defaultValue: Value // 默認(rèn)值
    private let storage: UserDefaults
    init(wrappedValue defaultValue: Value,
         key: String,
         storage: UserDefaults = .standard) {
        self.defaultValue = defaultValue
        self.key = key
        self.storage = storage
    }
}

使用如下:

struct SettingsViewModel {
    @UserDefaultsBacked(key: "mark-as-read") var autoMarkMessagesAsRead = true // 默認(rèn)值為true
    @UserDefaultsBacked(key: "search-page-size") var numberOfSearchResultsPerPage = 20 // 默認(rèn)值為20
}
    var setModelOne = SettingsViewModel() // 因?yàn)闆]有設(shè)置值,取到的未nil荷荤, 返回默默認(rèn)值
    var setModelTwo = SettingsViewModel() // 因?yàn)闆]有設(shè)置值退渗,取到的未nil, 返回默默認(rèn)值
    print(setModelOne.autoMarkMessagesAsRead) // true 由于wrappedValue是非可選蕴纳,打印不需要解包
    print(setModelTwo.numberOfSearchResultsPerPage) // 20 由于wrappedValue是非可選会油,打印不需要解包

上面的雖然利用默認(rèn)值解決了取值為可選項(xiàng)時(shí)為nil的特殊情況,但是賦值時(shí)依然只能是非可選的古毛,但是實(shí)際使用的過程中UserDefaults存儲的值類型很有可能是可選的翻翩,解決辦法是讓范型value遵守ExpressibleByNilLiteral協(xié)議(可以賦值為nil的字面量協(xié)議)。

extension UserDefaultsBacked where Value: ExpressibleByNilLiteral {
    init(key: String, storage: UserDefaults = .standard) {
        self.init(wrappedValue: nil, key: key, storage: storage)
    }
}
private protocol AnyOptional {
    var isNil: Bool { get }
}
extension Optional: AnyOptional {
    var isNil: Bool { self == nil }
}
@propertyWrapper struct UserDefaultsBacked<Value> {
    var wrappedValue: Value {
        get { ... }
        set {
            if let optional = newValue as? AnyOptional, optional.isNil {
                storage.removeObject(forKey: key)
            } else {
                storage.setValue(newValue, forKey: key)
            }
        }
    }
    ...
}
struct SettingsViewModel {
    @UserDefaultsBacked(key: "mark-as-read") var autoMarkMessagesAsRead = true
    @UserDefaultsBacked(key: "search-page-size")  var numberOfSearchResultsPerPage = 20
    @UserDefaultsBacked(key: "signature") var messageSignature: String?
}

代碼解讀:

  • 通過讓value遵守ExpressibleByNilLiteral協(xié)議稻薇,可以實(shí)現(xiàn)可選項(xiàng)賦值嫂冻。
  • 當(dāng)賦值為nil時(shí),則是從UserDefaults刪除key所對應(yīng)的鍵值塞椎,取到的為nil桨仿。

Projected values

有時(shí)候我們不需要直接拿到封裝后的值,而是需要拿到屬性本身案狠,簡而言之就是要引用被封裝的屬性本身服傍,下面我們通過二個(gè)實(shí)際開發(fā)中使用的例子來進(jìn)行說明暇昂。

Swift中由于沒有了像OC那樣的宏定義,無法直接通過宏定義配合pch文件來迭代版本伴嗡,但是Swift提供了條件編譯急波,利用條件編譯可以實(shí)現(xiàn)版本的控制,例如下面代碼在標(biāo)記為DATABASE_REALM時(shí)使用RealmDatabase數(shù)據(jù)庫瘪校,否則使用CoreDataDatabase數(shù)據(jù)庫澄暮,DATABASE_REALMswift無法通過宏定義,需要在Swift Compiler - Custom Flags > Active Compilation Conditions中進(jìn)行添加阱扬。

class DataBaseFactory {
    func makeDatabase() -> Database {
        #if DATABASE_REALM
        return RealmDatabase()
        #else
        return CoreDataDatabase()
        #endif
    }
}

但是當(dāng)標(biāo)記很多時(shí)泣懊,添加和刪除會(huì)很麻煩,實(shí)際在開發(fā)中通常使用靜態(tài)標(biāo)記Static flags麻惶,即自定義結(jié)構(gòu)體的形式來存儲我們的標(biāo)記flags馍刮。

struct FeatureFlags {
    let searchEnabled: Bool
    let maximumNumberOfFavorites: Int
    let allowLandscapeMode: Bool
}

通常采用Dic來配置我們FeatureFlagsDic可以是通過后臺返回窃蹋,也可以是本地保存的卡啰,下面的代碼采用了自動(dòng)閉包來設(shè)置默認(rèn)值,當(dāng)Dic中取值失敗時(shí)警没,利用自動(dòng)閉包返回default參數(shù)的默認(rèn)值匈辱。

extension FeatureFlags {
    init(dictionary: [String : Any]) {
        searchEnabled = dictionary.value(for: "search", default: false)
        maximumNumberOfFavorites = dictionary.value(for: "favorites", default: 10)
        allowLandscapeMode = dictionary.value(for: "landscape", default: true)
    }
}
private extension Dictionary where Key == String {
    func value<V>(for key: Key,
                  default defaultExpression: @autoclosure () -> V) -> V {
        return (self[key] as? V) ?? defaultExpression()
    }
}

利用FeatureFlags來進(jìn)行代碼的控制。

class FavoritesManager {
    private let featureFlags: FeatureFlags
    init(featureFlags: FeatureFlags) {
        self.featureFlags = featureFlags
    }
    func canUserAddMoreFavorites(_ user: User) -> Bool {
        let maxCount = featureFlags.maximumNumberOfFavorites
        return user.favorites.count < maxCount
    }
}

上面的代碼是對flag在開發(fā)中的使用的一個(gè)介紹杀迹,因?yàn)橄旅娴睦訒?huì)用到其中的知識,下面我們利用flagProperty warppers來進(jìn)行一個(gè)實(shí)際例子的演練亡脸,其中會(huì)利用到。

定義一個(gè)名為Flag的屬性封裝器树酪,這里我們用到了projectedValue這個(gè)屬性浅碾,返回的是Flag屬性封裝器本身,可以理解為對屬性封裝器的引用续语,在swfitUI中經(jīng)常使用垂谢,要拿到引用的指針地址需要利用$符號,當(dāng)然同樣可以用在UIKit中绵载。

@propertyWrapper final class Flag<Value> {
    var wrappedValue: Value
    let name: String
    fileprivate init(wrappedValue: Value, name: String) {
        self.wrappedValue = wrappedValue
        self.name = name
    }
    var projectedValue: Flag { self }
}

在實(shí)際開發(fā)中Decodable Flag的值一般來自網(wǎng)絡(luò)請求服務(wù)器返回的數(shù)據(jù)埂陆,讓Flag遵循Decodable協(xié)議以便直接將后臺返回的數(shù)據(jù)轉(zhuǎn)換成Flag模型苛白,也就是所說的反序列化娃豹。

// 定義keys
private struct FlagCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init(name: String) {
        stringValue = name
    }
    // 下面的初始化器是CodingKey協(xié)議必須實(shí)現(xiàn)的
    init?(stringValue: String) {
        self.stringValue = stringValue
    }
    init?(intValue: Int) {
        self.intValue = intValue
        self.stringValue = String(intValue)
    }
}
private protocol DecodableFlag {
    typealias Container = KeyedDecodingContainer<FlagCodingKey>
    func decodeValue(from container: Container) throws
}
extension FeatureFlags: Decodable {
  // 這個(gè)初始化方法之前是編譯器默認(rèn)生成的
    init(from decoder: Decoder) throws {
      // container它本質(zhì)上是一個(gè)特殊的字典,只允許你訪問具有特定鍵的值购裙,把key放進(jìn)這個(gè)dic中
        let container = try decoder.container(keyedBy: FlagCodingKey.self)
     // Mirror可以映射一個(gè)對象,方便觀察這個(gè)對象的屬性和值
        for child in Mirror(reflecting: self).children {
            // child.value是映像對象的屬性值
            guard let flag = child.value as? DecodableFlag else {
                continue
            }
            try flag.decodeValue(from: container)
        }
    }
}
extension Flag: DecodableFlag where Value: Decodable {
    fileprivate func decodeValue(from container: Container) throws {
        // 注入key值
        let key = FlagCodingKey(name: name)
        if let value = try container.decodeIfPresent(Value.self, forKey: key) {
            wrappedValue = value
        }
    }
}

代碼解讀:

  • 利用Codekey協(xié)議自定義了Decode解碼懂版,解碼的keyFlagname屬性,同時(shí)FlagCodingKey采用結(jié)構(gòu)體的方式定義而非枚舉的方式躏率。
  • 采用decodeIfPresent方法進(jìn)行解碼躯畴,放對應(yīng)的key不存在或者key對應(yīng)的值為空時(shí)解碼返回的是nil對象民鼓。
  • 解碼的container容器采用的是KeyedEncodingContainer普通的容器。

使用:

首先創(chuàng)建FeatureFlags的靜態(tài)Static flags蓬抄,里面的屬性采用了@Flag進(jìn)行封裝丰嘉。

struct FeatureFlags{
    @Flag(name: "feature-search")
    var isSearchEnabled = false
    @Flag(name: "experiment-note-limit")
    var maximumNumberOfNotes = 999
}

創(chuàng)建一個(gè)button按鈕,點(diǎn)擊后跳轉(zhuǎn)頁面嚷缭,并利用$符號將isSearchEnabled這個(gè)封裝的屬性傳給了下個(gè)頁面的flag屬性饮亏。

import UIKit
class ViewController: UIViewController {
    var flag:FeatureFlags?
    override func viewDidLoad() {
        super.viewDidLoad()
       // swfit中“”“包裹可以保留所有格式,這里是模擬服務(wù)器返回的flag數(shù)據(jù)
        let jsonString = """ { "feature-search": false,"experiment-note-limit": 3 } """
        if let data = jsonString.data(using: .utf8) {
            let decoder = JSONDecoder()
      // 直接進(jìn)行解碼阅爽,解碼類型為FeatureFlags類型
            if let flags = try? decoder.decode(FeatureFlags.self, from: data) {
                flag = flags
            }
        }
        let button = UIButton.init(type: UIButton.ButtonType.system)
        button.tintColor = UIColor.black
        button.setTitle("點(diǎn)擊", for: UIControl.State.normal)
        button.frame = CGRect.init(x: 100, y: 100, width: 150, height: 100)
        self.view.addSubview(button)
        button.addTarget(self, action: #selector(goToFlagVC), for: .touchUpInside)
    }
    @objc func goToFlagVC() {
        let searchToggleVC = FlagToggleViewController(
            // 將封裝器的實(shí)例傳給了VC路幸,這會(huì)使得VC可以更改封裝器中屬性的值
            flag: flag!.$isSearchEnabled
        )
        self.present(searchToggleVC, animated: true, completion:nil)
    }
}

代碼中用jsonString字符串模擬了后臺返回的數(shù)據(jù),"feature-search"字段為false付翁,當(dāng)點(diǎn)擊按鈕后简肴,跳轉(zhuǎn)頁面并將isSearchEnabled屬性傳遞過去。

import UIKit
import Foundation
class FlagToggleViewController: UIViewController {
    private let flag: Flag<Bool>
    private lazy var label = UILabel()
    private lazy var toggle = UISwitch()
    init(flag: Flag<Bool>) {
        self.flag = flag
        super.init(nibName: nil, bundle: nil)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white
        label.text = flag.name
        label.frame = CGRect.init(x: 100, y: 100, width: 150, height: 150)
        toggle.frame = CGRect.init(x: 100, y: 200, width: 150, height: 150)
        toggle.isOn = flag.wrappedValue
        self.view.addSubview(label)
        self.view.addSubview(toggle)
        toggle.addTarget(self,
            action: #selector(toggleFlag),
            for: .valueChanged
        )
    }
    @objc private func toggleFlag() {
        flag.wrappedValue = toggle.isOn
    }
}

點(diǎn)擊toggle后百侧,會(huì)改變flag.wrappedValue的值砰识,由于使用的是$符號進(jìn)行傳遞,是引用傳遞了屬性本身佣渴,這會(huì)使得ViewController中的flag屬性中的wrappedValue的值也會(huì)發(fā)生改變仍翰,當(dāng)FlagToggleViewController頁面disappear后,再次appear時(shí)观话,會(huì)顯示上次toggle操作后的結(jié)果予借。

自定義Keychain的 property wrapper

SwfitUI中提供了SceneStorageAppStorage二個(gè)屬性封裝器來從scene memoryuser defaults中獲取數(shù)據(jù),在上文中我們也演示了AppStorage的使用频蛔,但是Swift沒有從Keychain鑰匙串中獲取數(shù)據(jù)的屬性封裝器灵迫,接下來我們自定義一個(gè)實(shí)現(xiàn)所需的功能。

  • 定義propertyWrapper
@propertyWrapper struct SecureStorage<Value: Codable>: DynamicProperty {
    @StateObject private var storage: KeychainStorage<Value>
    var wrappedValue: Value {
        get { storage.value }
        nonmutating set {
            storage.value = newValue
        }
    }
    init(wrappedValue: Value, _ key: String) {
    // 注意這里對storage的初始化晦溪,由于storage是StateObject類型瀑粥,所以初始化需要采用StateObject封裝器 
    // 內(nèi)部的初始化方法
            self._storage = StateObject(
            wrappedValue: KeychainStorage(
                defaultValue: wrappedValue,
                for: key
            )
        )
    }
    var projectedValue: Binding<Value> {
        .init(
            get: { wrappedValue },
            set: { wrappedValue = $0 }
        )
    }
}

代碼解讀;

  • 屬性封裝器內(nèi)依然可以使用原生的屬性封裝器,里面用@StateObject定義了一個(gè)storage屬性三圆,其功能是用來在鑰匙串中存取數(shù)據(jù)狞换。
  • 需注意storage的初始化,這里是調(diào)用了StateObjectinit(wrappedValue:)方法舟肉。
  • projectedValue可以遵循Binding等協(xié)議修噪,這里遵守Binging協(xié)議并實(shí)現(xiàn)了init方法,通過準(zhǔn)守Binging協(xié)議路媚,則可用$符號將屬性封裝的引用傳遞給子View使用黄琼,子View則使用@Binding進(jìn)行綁定來同步修改封裝的屬性值。
import Foundation
import KeychainAccess
import Combine
private final class KeychainStorage<Value: Codable>: ObservableObject {
    var value: Value {
        set {
            objectWillChange.send()
            save(newValue)
        }
        get { fetch() }
    }
    let objectWillChange = PassthroughSubject<Void, Never>()
    private let key: String
    private let defaultValue: Value
    private let decoder = JSONDecoder()
    private let encoder = JSONEncoder()

    private let keychain = Keychain(
        service: "com.mamba.444444",
        accessGroup: "7123456BR56.mambaGroup"
    )
        .synchronizable(true)
        .accessibility(.always)
    init(defaultValue: Value, for key: String) {
        self.defaultValue = defaultValue
        self.key = key
    }
    private func save(_ newValue: Value) {
        guard let data = try? encoder.encode(newValue) else {
            return
        }
        try? keychain.set(data, key: key)
    }
    private func fetch() -> Value {
        guard
            let data = try? keychain.getData(key),
            let freshValue = try? decoder.decode(Value.self, from: data)
        else {
            return defaultValue
        }
        return freshValue
    }
}

代碼解讀

  • 使用了第三方庫KeychainAccess來管理鑰匙串整慎。
  • KeychainStorage遵守了ObservableObject協(xié)議脏款,并采用PassthroughSubject來當(dāng)value進(jìn)行調(diào)用set方法時(shí)publish事件围苫,這會(huì)觸發(fā)SecureStorage中的@StateObject修飾的storage屬性接受事件,并告知SwiftUI對使用了storage屬性值相關(guān)聯(lián)的一切界面進(jìn)行界面的刷新撤师。

使用:

import SwiftUI
struct ContentView: View {
    @SecureStorage("mamba")
    var goal: Int = 150
    var body: some View {
        NavigationView {
            VStack {
                Section(header: Text("heartMinutesGoal")) {
                    Stepper(value: $goal, in: 150...900, step: 10) {
                        Text("\(goal) heartMinutesGoalValue")
                    }
                }
                NavigationLink(destination: subPageView(goal: $goal)){
                    Text("點(diǎn)擊跳轉(zhuǎn)")
                }
            }
        }
    }
}

goal屬性采用SecureStorage進(jìn)行了修飾剂府,并設(shè)置初始值為150,當(dāng)點(diǎn)擊Steppe進(jìn)行增加操作后剃盾,當(dāng)前頁面的Text會(huì)同步更新周循,點(diǎn)擊跳轉(zhuǎn)按鈕后會(huì)進(jìn)入subPageView頁面。

import SwiftUI
struct subPageView: View {
    @Binding var goal: Int;
    var body: some View {
        Section(header: Text("subMinutesGoal")) {
            Stepper(value: $goal, in: 150...900, step: 10) {
                Text("\(goal) subMinutesGoalValue")
            }
        }
    }
}

subPageView頁面利用Binging對上個(gè)頁面?zhèn)鱽淼?strong>goal進(jìn)行了綁定万俗,這樣就可以修改上個(gè)頁面的值湾笛,當(dāng)點(diǎn)擊當(dāng)前頁面的Stepper進(jìn)行goal值的加減操作后,值會(huì)因?yàn)?strong>SecureStorage封裝器背后的邏輯存儲到鑰匙串中闰歪,當(dāng)回退到上個(gè)頁面時(shí)嚎研,goal的值會(huì)保持最新狀態(tài),關(guān)掉App重新進(jìn)入時(shí)库倘,值也會(huì)顯示為最新狀態(tài)临扮。

這里需要在KeychainStorage類中填入自己開發(fā)者賬號的accessGroup,一般為開發(fā)者AppIdentifierPrefix加上工程中配置的group名字教翩,類似為"7123456BR56.mambaGroup"杆勇。

獲取property wrapper’s enclosing instance

在開發(fā)中有時(shí)不進(jìn)需要獲取property wrapper包裝后的wrappedValue和包裝屬性的本身projectedValue,還需要能獲取擁有這個(gè)包裝屬性的實(shí)例本身饱亿,從而能使用這個(gè)實(shí)例的其他屬性蚜退,swift默認(rèn)是將property wrapper和實(shí)例進(jìn)行隔開的,但是還是隱藏的開放了相關(guān)API供開發(fā)者使用彪笼。上文中我們多次提到钻注,在使用property wrapper時(shí),我們需要定義一個(gè)wrappedValue屬性配猫。但其實(shí)還能通過static subscript來處理wrappedValue幅恋,像下面這樣:

@propertyWrapper
struct EnclosingTypeReferencingWrapper<Value> {
    static subscript<T>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        ...
    }
    ...
}

利用Swift’s key paths feature可以同時(shí)獲取實(shí)例本身 wrapper itself和包裝后的值wrappedValue,但是實(shí)例本身必須是class類型的泵肄,因?yàn)樯厦娴?strong>subscript使用的是可讀可寫的ReferenceWritableKeyPath捆交,這是引用類型。為了避免使用struct類型的也通過ReferenceWritableKeyPath來修改屬性腐巢,同樣在開發(fā)中會(huì)這樣:

@propertyWrapper
struct EnclosingTypeReferencingWrapper<Value> {
    static subscript<T>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        ...
    }
    @available(*, unavailable,
    message: "This property wrapper can only be applied to classes"
)
var wrappedValue: Value {
    get { fatalError() }
    set { fatalError() }
}
}

假如想讓下面的text屬性和label進(jìn)行綁定品追,即只要設(shè)置text屬性,lable顯示的內(nèi)容也會(huì)發(fā)生改變系忙,該如何做诵盼?當(dāng)然可以利用傳統(tǒng)的通知實(shí)現(xiàn),但是每當(dāng)有一個(gè)新的text屬性银还,就要重新寫一份邏輯代碼风宁,重用率不高,這里就能利用property wrapper’s enclosing instance進(jìn)行實(shí)現(xiàn)蛹疯。

class Parent: UIView {
    private let label = UILabel()
    @Derived(\Parent.label.text) var text: String?
}

定義PropertyWrapper

@propertyWrapper
struct AnyDerived<Instance, Value> {
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    private let getter: (Instance) -> Value
    private let setter: (Instance, Value) -> Void
    init(_ keyPath: ReferenceWritableKeyPath<Instance, Value>) {
        getter = { $0[keyPath: keyPath] }
        setter = { $0[keyPath: keyPath] = $1 }
    }
    static subscript(
        _enclosingInstance instance: Instance,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<Instance, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<Instance, Self>
    ) -> Value {
        get { instance[keyPath: storageKeyPath].getter(instance) }
        set { instance[keyPath: storageKeyPath].setter(instance, newValue) }
    }
 @available(*, unavailable,
    message: "This property wrapper can only be applied to classes"
}
protocol ProxyContainer {
    typealias Derived<T> = AnyDerived<Self, T>
}
extension NSObject: ProxyContainer {}

使用如下:

class Parent: UIView {
    private let label = UILabel()
    @Derived(\.label.text) var text: String?
}
let instance = Parent()
instance.text // nil
instance.text = "foo"
label.text // foo

總結(jié):

本文從wrappedValueprojectedValue戒财,再到enclosing instance逐步的闡述了Property warppers的內(nèi)部實(shí)現(xiàn)原理,并結(jié)合實(shí)際Demo進(jìn)行了相關(guān)演示捺弦,利用Property warppers的特性可以提高代碼的復(fù)用率饮寞,也可以幫助我們更好的理解SwiftUI中例如StateBinding等關(guān)鍵詞的工作原理列吼,在實(shí)際開發(fā)中合理的運(yùn)用Property warppers能達(dá)到事半功倍的效果幽崩。
Demo1地址 Demo2地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市寞钥,隨后出現(xiàn)的幾起案子慌申,更是在濱河造成了極大的恐慌,老刑警劉巖理郑,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹄溉,死亡現(xiàn)場離奇詭異,居然都是意外死亡您炉,警方通過查閱死者的電腦和手機(jī)柒爵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赚爵,“玉大人棉胀,你說我怎么就攤上這事〖较ィ” “怎么了膏蚓?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長畸写。 經(jīng)常有香客問我驮瞧,道長,這世上最難降的妖魔是什么枯芬? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任论笔,我火速辦了婚禮,結(jié)果婚禮上千所,老公的妹妹穿的比我還像新娘狂魔。我一直安慰自己,他們只是感情好淫痰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布最楷。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪籽孙。 梳的紋絲不亂的頭發(fā)上烈评,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機(jī)與錄音犯建,去河邊找鬼讲冠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛适瓦,可吹牛的內(nèi)容都是我干的竿开。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼玻熙,長吁一口氣:“原來是場噩夢啊……” “哼否彩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嗦随,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤列荔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后称杨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肌毅,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年姑原,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了悬而。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锭汛,死狀恐怖笨奠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情唤殴,我是刑警寧澤般婆,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站朵逝,受9級特大地震影響蔚袍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜配名,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一啤咽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渠脉,春花似錦宇整、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霸饲。三九已至,卻和暖如春臂拓,著一層夾襖步出監(jiān)牢的瞬間厚脉,已是汗流浹背绽族。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留福稳,地道東北人妇斤。 一個(gè)月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像芬失,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容