Objective-C 最讓人詬病的也許就是不能給已有類新增屬性, 但是可以通過 Objective-C 的執行時機制關聯自定義屬性到物件上, 幾乎彌補了這個痛點.
Swift Extension 比 Objective-C Category 增色不少, extension 能夠給已有類新增計算型屬性, 這已經是很大的進步, 但是仍然不能新增儲存屬性. Swift 中也可以使用 Objective-C runtime 的關聯物件(Associated Objects)的方式新增屬性, 彌補這一痛點.
關聯物件(Associated Objects)
Swift 中提供三個與 Objective-C 類似的方法將自定義的屬性關聯到物件上:
objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects
注意: 使用 objc_removeAssociatedObjects 時要小心, 這個方法會刪除物件關聯的所有屬性, 就可能導致把別人新增的關聯屬性也刪掉. 如果要刪除某一個屬性, 使用 objc_setAssociatedObject 方法, value 置為 nil.
下面給 UIView 新增三種不同型別的屬性: isShow, displayName, width.
extension UIView {
// 巢狀結構體
private struct AssociatedKeys {
static var isShowKey = "isShowKey"
static var displayNameKey = "displayNameKey"
static var widthKey = "widthKey"
}
// Bool 型別
var isShow: Bool {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.isShowKey) as! Bool
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.isShowKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
// String 型別
var displayName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.displayNameKey) as? String
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.displayNameKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
// Float 型別
var width: Float {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.widthKey) as! Float
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.widthKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
複製程式碼
上述列子說明幾點:
- 巢狀私有結構體, 宣告與擴充套件屬性對應的鍵(key). Swift Extension 提供了豐富的功能, 可以在 Extension 中巢狀型別, 使用 private 私有訪問控制, 不會汙染整個名稱空間, 而且能夠統一管理關聯物件鍵.
- Swift 的基本型別Int, Float, Double, Bool能夠自動隱式地轉換成 Objective-C 的 NSNumber 型別, 所以不需要顯示的包裝成 NSNumber 型別進行關聯.
- 如果使用
OBJC_ASSOCIATION_ASSIGN
關聯策略時要注意, 文件中指出是弱引用, 但不完全等同於 weak, 更像是 unsafe_unretained 引用, 關聯物件被釋放後,關聯屬性仍然保留被釋放的地址, 如果不小心訪問關聯屬性, 就會造成野指標訪問出錯.
Specifies a weak reference to the associated object.
抽取關聯物件方法
我們可以把關聯物件的方法提取成公共方法, 在 NSObject 類的 extension 裡實現, 只要繼承自 NSObject 的類就能夠呼叫關聯物件方法, 通過Swift 泛型來關聯不同型別的屬性.
extension NSObject {
func setAssociated<T>(value: T, associatedKey: UnsafeRawPointer, policy: objc_AssociationPolicy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) -> Void {
objc_setAssociatedObject(self, associatedKey, value, policy)
}
func getAssociated<T>(associatedKey: UnsafeRawPointer) -> T? {
let value = objc_getAssociatedObject(self, associatedKey) as? T
return value;
}
}
複製程式碼
我們只需要在 UIView+Extension.swift 中調動上面兩個方法即可, 目前只支援有可選型別的屬性.
extension UIView {
private struct AssociatedKeys {
static var displayNameKey = "displayNameKey"
}
var displayName: String? {
get {
return getAssociated(associatedKey: &AssociatedKeys.displayNameKey)
}
set {
setAssociated(value: newValue, associatedKey: &AssociatedKeys.displayNameKey)
}
}
}
複製程式碼
關聯閉包屬性
開發中有時會給已有類關聯閉包屬性, 比如給 UIViewController 類新增一個 pushCompletion 的閉包屬性, 當導航控制器 push 動作完成後呼叫該控制器的 pushCompletion 閉包.
先按照最基本的方式來關聯物件, 如下:
typealias pushCompletionClosure = ()->()
extension UIViewController {
private struct AssociatedKeys {
static var pushCompletionKey = "pushCompletionKey"
}
var pushCompletion: pushCompletionClosure? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.pushCompletionKey) as? pushCompletionClosure
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.pushCompletionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
複製程式碼
開開心心編譯一發, 發現編譯報錯:
- 使用泛型包裝閉包屬性, 利用 NSObject+Extension.swift 中的
setAssociated
方法來關聯閉包. - 建立私有閉包容器類, 利用閉包容器間接關聯閉包屬性.
泛型包裝閉包屬性
setAssociated 方法需要泛型引數, 當傳入閉包後, 就會把閉包包裝成泛型.
set {
setAssociated(value: newValue, associatedKey: &AssociatedKeys.pushCompletionKey)
}
複製程式碼
閉包容器
使用閉包容器的方式關聯閉包屬性, 過程分為兩步:
- 在 extension 中巢狀建立容器類, 容器類中定義需要關聯的閉包屬性.
- 關聯物件時把容器類物件關聯到已有類, 間接的就把閉包屬性關聯到已有類.
閉包容器的方式是把閉包屬性包裝到了容器中, 再把容器物件關聯到已有類上, 跟泛型包裝閉包有異曲同工之處, 因此必須通過容器物件來訪問閉包, 如果需要給類關聯的閉包屬性相對較多, 這種方式也不失為一種好方法, 能統一管理閉包屬性, 程式碼層級結構也比較清晰.
typealias pushCompletionClosure = ()->()
extension UIViewController {
private struct AssociatedKeys {
static var pushCompletionKey = "pushCompletionKey"
}
// 巢狀閉包容器類
class closureContainer {
var pushCompletion: pushCompletionClosure?
}
// 關聯容器屬性
var container: closureContainer? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.pushCompletionKey) as? closureContainer
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.pushCompletionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
複製程式碼
程式碼在這裡:
github
歡迎大家留言斧正!