閱讀時(shí)長35分鐘罩句,Swift5.0
開始支持Property warppers
屬性包裝器,顧名思義就是對屬性進(jìn)行包裝李皇,給屬性附加一段邏輯祟印,同時(shí)對這段邏輯操作進(jìn)行了封裝肴沫,在背后透明的運(yùn)行,這樣的好處是能大大的增加代碼的重用率蕴忆。目前Apple
的SwiftUI
框架就是用了很多屬性包裝器如常用的@State
颤芬,@Binding
,@Appstroe
套鹅,@StateObject
站蝠,@ObserveObjec
t和@EnvironmentObject
等來封裝屬性邏輯并進(jìn)行監(jiān)聽,當(dāng)屬性發(fā)生改變時(shí)同步的刷新UI
芋哭,通過本文你將會(huì)學(xué)習(xí)到一下內(nèi)容:
- 了解
SwiftUI
原生Property warppers
屬性包裝器的實(shí)現(xiàn)邏輯沉衣。 - 理解
Property warppers
的本質(zhì)。 - 用
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
屬性毅舆,這樣涉及firstName
和lastName
屬性的讀寫操作其實(shí)是操作對應(yīng)的屬性包裝器中的wrappedValue
屬性西篓,這樣就實(shí)現(xiàn)了在給firstName
和lastName
屬性賦值后,背后的邏輯已經(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ā)屬性。
觀察屬性包裝器由于其定義是struct
和class
粱甫,所有也能擁有自己的屬性泳叠,能實(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)定義了key
和storage
屬性,其中storage
可以讓屬性包裝器實(shí)現(xiàn)依賴注入瞒大。
雖然上面autoMarkMessagesAsRead
和numberOfSearchResultsPerPage
二個(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_REALM
在swift
無法通過宏定義,需要在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
來配置我們FeatureFlags
,Dic
可以是通過后臺返回窃蹋,也可以是本地保存的卡啰,下面的代碼采用了自動(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ì)用到其中的知識,下面我們利用flag
和Property 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
解碼懂版,解碼的key
是Flag
的name
屬性,同時(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
中提供了SceneStorage
和AppStorage
二個(gè)屬性封裝器來從scene memory
和user 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)用了StateObject
的init(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é):
本文從wrappedValue
到projectedValue
戒财,再到enclosing instance
逐步的闡述了Property warppers
的內(nèi)部實(shí)現(xiàn)原理,并結(jié)合實(shí)際Demo
進(jìn)行了相關(guān)演示捺弦,利用Property warppers
的特性可以提高代碼的復(fù)用率饮寞,也可以幫助我們更好的理解SwiftUI
中例如State
,Binding
等關(guān)鍵詞的工作原理列吼,在實(shí)際開發(fā)中合理的運(yùn)用Property warppers
能達(dá)到事半功倍的效果幽崩。
Demo1地址 Demo2地址