如何在 Swift 中存储属性,就像我在 Objective-C 中那样?

我正在将一个应用程序从 Objective-C 切换到 Swift,我有几个类别的存储属性,例如:

@interface UIView (MyCategory)


- (void)alignToView:(UIView *)view
alignment:(UIViewRelativeAlignment)alignment;
- (UIView *)clone;


@property (strong) PFObject *xo;
@property (nonatomic) BOOL isAnimating;


@end

由于 Swift 扩展不接受这样的存储属性,我不知道如何维护与 Object 代码相同的结构。存储属性对我的应用程序来说非常重要,我相信苹果一定已经在 Swift 中创建了一些解决方案。

正如 jou 所说,我所寻找的实际上是使用相关对象,所以我这样做了(在另一个上下文中) :

import Foundation
import QuartzCore
import ObjectiveC


extension CALayer {
var shapeLayer: CAShapeLayer? {
get {
return objc_getAssociatedObject(self, "shapeLayer") as? CAShapeLayer
}
set(newValue) {
objc_setAssociatedObject(self, "shapeLayer", newValue, UInt(OBJC_ASSOCIATION_RETAIN))
}
}


var initialPath: CGPathRef! {
get {
return objc_getAssociatedObject(self, "initialPath") as CGPathRef
}
set {
objc_setAssociatedObject(self, "initialPath", newValue, UInt(OBJC_ASSOCIATION_RETAIN))
}
}
}

但是我得到了一个 EXC _ BAD _ ACCESS:

class UIBubble : UIView {
required init(coder aDecoder: NSCoder) {
...
self.layer.shapeLayer = CAShapeLayer()
...
}
}

有什么想法吗?

129958 次浏览

不能使用新存储定义类别(Swift 扩展) ; 任何其他属性必须是 计算出来的而不是存储。这种语法适用于 Objective C,因为类别中的 @property实际上意味着“我将提供 getter 和 setter”。在 Swift 中,您需要自己定义这些属性来获得一个计算属性; 类似于:

extension String {
public var Foo : String {
get
{
return "Foo"
}


set
{
// What do you want to do here?
}
}
}

应该可以正常工作。请记住,不能在 setter 中存储新值,只能使用现有的可用类状态。

对于 Obj-c 类别,您只能添加方法,而不能添加实例变量。

在您的示例中,您使用@property 作为添加 getter 和 setter 方法声明的快捷方式。您仍然需要实现那些方法。

类似地,在 Swift 中可以添加使用扩展来添加实例方法、计算属性等,但不能添加存储属性。

与 Objective-C 一样,您不能将存储属性添加到现有类中。如果您正在扩展 Objective-C 类(UIView肯定是其中之一) ,您仍然可以使用 相关对象来模拟存储属性:

为了 Swift 1

import ObjectiveC


private var xoAssociationKey: UInt8 = 0


extension UIView {
var xo: PFObject! {
get {
return objc_getAssociatedObject(self, &xoAssociationKey) as? PFObject
}
set(newValue) {
objc_setAssociatedObject(self, &xoAssociationKey, newValue, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN))
}
}
}

关联键是一个指针,它应该是每个关联的唯一指针。为此,我们创建了一个私有全局变量,并使用它的内存地址作为 &操作符的键。参见 http://developer.apple.com/library/pre-release/ios/document/Swift/Concept/BuildingCocoaApps/InteractingWithCAPIs.html #//apple _ ref/doc/uid/TP40014216-CH8-XID _ 15”rel = “ noReferrer”> 使用 Swift 和 Cocoa 以及 Objective-C 关于在 Swift 中如何处理指针的更多细节。

更新为 Swift 2和3

import ObjectiveC


private var xoAssociationKey: UInt8 = 0


extension UIView {
var xo: PFObject! {
get {
return objc_getAssociatedObject(self, &xoAssociationKey) as? PFObject
}
set(newValue) {
objc_setAssociatedObject(self, &xoAssociationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}

更新为 Swift 4

在 Swift 4中,要简单得多。Holder 结构将包含我们的计算属性将向全世界公开的私有值,从而给人一种存储属性行为的错觉。

来源

extension UIViewController {
struct Holder {
static var _myComputedProperty:Bool = false
}
var myComputedProperty:Bool {
get {
return Holder._myComputedProperty
}
set(newValue) {
Holder._myComputedProperty = newValue
}
}
}

Jou指出的解决方案不支持 值类型, 这对他们也很有效

包装纸

import ObjectiveC


final class Lifted<T> {
let value: T
init(_ x: T) {
value = x
}
}


private func lift<T>(x: T) -> Lifted<T>  {
return Lifted(x)
}


func setAssociatedObject<T>(object: AnyObject, value: T, associativeKey: UnsafePointer<Void>, policy: objc_AssociationPolicy) {
if let v: AnyObject = value as? AnyObject {
objc_setAssociatedObject(object, associativeKey, v,  policy)
}
else {
objc_setAssociatedObject(object, associativeKey, lift(value),  policy)
}
}


func getAssociatedObject<T>(object: AnyObject, associativeKey: UnsafePointer<Void>) -> T? {
if let v = objc_getAssociatedObject(object, associativeKey) as? T {
return v
}
else if let v = objc_getAssociatedObject(object, associativeKey) as? Lifted<T> {
return v.value
}
else {
return nil
}
}

有可能 类扩展 (使用示例) :

extension UIView {


private struct AssociatedKey {
static var viewExtension = "viewExtension"
}


var referenceTransform: CGAffineTransform? {
get {
return getAssociatedObject(self, associativeKey: &AssociatedKey.viewExtension)
}


set {
if let value = newValue {
setAssociatedObject(self, value: value, associativeKey: &AssociatedKey.viewExtension, policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}

这确实是一个非常棒的解决方案,我想添加另一个使用示例,其中包括非可选的结构和值。此外,还可以简化 AssociatedKey 值。

struct Crate {
var name: String
}


class Box {
var name: String


init(name: String) {
self.name = name
}
}


extension UIViewController {


private struct AssociatedKey {
static var displayed:   UInt8 = 0
static var box:         UInt8 = 0
static var crate:       UInt8 = 0
}


var displayed: Bool? {
get {
return getAssociatedObject(self, associativeKey: &AssociatedKey.displayed)
}


set {
if let value = newValue {
setAssociatedObject(self, value: value, associativeKey: &AssociatedKey.displayed, policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}


var box: Box {
get {
if let result:Box = getAssociatedObject(self, associativeKey: &AssociatedKey.box) {
return result
} else {
let result = Box(name: "")
self.box = result
return result
}
}


set {
setAssociatedObject(self, value: newValue, associativeKey: &AssociatedKey.box, policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}


var crate: Crate {
get {
if let result:Crate = getAssociatedObject(self, associativeKey: &AssociatedKey.crate) {
return result
} else {
let result = Crate(name: "")
self.crate = result
return result
}
}


set {
setAssociatedObject(self, value: newValue, associativeKey: &AssociatedKey.crate, policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

我更喜欢用纯 Swift 编写代码,而不依赖 Objective-C 传统。正因为如此,我写了纯 Swift 解决方案,它有两个优点和两个缺点。

优点:

  1. 纯斯威夫特密码

  2. 可用于类和完成,或者更具体地用于 Any对象

缺点:

  1. 代码应该调用方法 willDeinit()来释放链接到特定类实例的对象,以避免内存泄漏

  2. 对于本例,不能直接对 UIView 进行扩展,因为 var frame是对 UIView 的扩展,而不是类的一部分。

编辑:

import UIKit


var extensionPropertyStorage: [NSObject: [String: Any]] = [:]


var didSetFrame_ = "didSetFrame"


extension UILabel {


override public var frame: CGRect {


get {
return didSetFrame ?? CGRectNull
}


set {
didSetFrame = newValue
}
}


var didSetFrame: CGRect? {


get {
return extensionPropertyStorage[self]?[didSetFrame_] as? CGRect
}


set {
var selfDictionary = extensionPropertyStorage[self] ?? [String: Any]()


selfDictionary[didSetFrame_] = newValue


extensionPropertyStorage[self] = selfDictionary
}
}


func willDeinit() {
extensionPropertyStorage[self] = nil
}
}

所以我想我找到了一个比上面的方法更简洁的方法,因为它不需要任何全局变量。接下来交给我吧: Http://nshipster.com/swift-objc-runtime/

要点是您可以使用这样的结构:

extension UIViewController {
private struct AssociatedKeys {
static var DescriptiveName = "nsh_DescriptiveName"
}


var descriptiveName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.DescriptiveName) as? String
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.DescriptiveName,
newValue as NSString?,
UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)
)
}
}
}
}

更新 Swift 2

private struct AssociatedKeys {
static var displayed = "displayed"
}


//this lets us check to see if the item is supposed to be displayed or not
var displayed : Bool {
get {
guard let number = objc_getAssociatedObject(self, &AssociatedKeys.displayed) as? NSNumber else {
return true
}
return number.boolValue
}


set(value) {
objc_setAssociatedObject(self,&AssociatedKeys.displayed,NSNumber(bool: value),objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

我的0.02美元,这个代码是用 Swift 2.0写的

extension CALayer {
private struct AssociatedKeys {
static var shapeLayer:CAShapeLayer?
}


var shapeLayer: CAShapeLayer? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.shapeLayer) as? CAShapeLayer
}
set {
if let newValue = newValue {
objc_setAssociatedObject(self, &AssociatedKeys.shapeLayer, newValue as CAShapeLayer?, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}

我尝试了许多解决方案,发现这是实际扩展带有额外变量参数的类的唯一方法。

我还得到一个 EXC _ BAD _ ACCESS 问题。objc_getAssociatedObject()objc_setAssociatedObject()中的值应该是 Object。objc_AssociationPolicy应该与 Object 匹配。

我尝试使用 obc _ setAssociatedObject,正如这里的一些答案所提到的,但是在失败了几次之后,我后退了一步,意识到我没有理由需要它。借鉴这里的一些想法,我想出了这个代码,它只是存储一个数组,无论我的额外数据是(在这个例子中是 MyClass)索引的对象,我想关联它:

class MyClass {
var a = 1
init(a: Int)
{
self.a = a
}
}


extension UIView
{
static var extraData = [UIView: MyClass]()


var myClassData: MyClass? {
get {
return UIView.extraData[self]
}
set(value) {
UIView.extraData[self] = value
}
}
}


// Test Code: (Ran in a Swift Playground)
var view1 = UIView()
var view2 = UIView()


view1.myClassData = MyClass(a: 1)
view2.myClassData = MyClass(a: 2)
print(view1.myClassData?.a)
print(view2.myClassData?.a)

我试图通过使用 objecc _ getAssociatedObject,objecc _ setAssociatedObject 来存储属性,但没有任何进展。我的目标是为 UITextField 创建扩展,以验证文本输入字符长度。 下面的代码对我来说很好用。希望这能对某些人有所帮助。

private var _min: Int?
private var _max: Int?


extension UITextField {
@IBInspectable var minLength: Int {
get {
return _min ?? 0
}
set {
_min = newValue
}
}


@IBInspectable var maxLength: Int {
get {
return _max ?? 1000
}
set {
_max = newValue
}
}


func validation() -> (valid: Bool, error: String) {
var valid: Bool = true
var error: String = ""
guard let text = self.text else { return (true, "") }


if text.characters.count < minLength {
valid = false
error = "Textfield should contain at least \(minLength) characters"
}


if text.characters.count > maxLength {
valid = false
error = "Textfield should not contain more then \(maxLength) characters"
}


if (text.characters.count < minLength) && (text.characters.count > maxLength) {
valid = false
error = "Textfield should contain at least \(minLength) characters\n"
error = "Textfield should not contain more then \(maxLength) characters"
}


return (valid, error)
}
}

我发现这个解决方案更实用

更新为 Swift 3

extension UIColor {


static let graySpace = UIColor.init(red: 50/255, green: 50/255, blue: 50/255, alpha: 1.0)
static let redBlood = UIColor.init(red: 102/255, green: 0/255, blue: 0/255, alpha: 1.0)
static let redOrange = UIColor.init(red: 204/255, green: 17/255, blue: 0/255, alpha: 1.0)


func alpha(value : CGFloat) -> UIColor {
var r = CGFloat(0), g = CGFloat(0), b = CGFloat(0), a = CGFloat(0)
self.getRed(&r, green: &g, blue: &b, alpha: &a)
return UIColor(red: r, green: g, blue: b, alpha: value)
}


}

然后在你的代码里

class gameController: UIViewController {


@IBOutlet var game: gameClass!


override func viewDidLoad() {
self.view.backgroundColor = UIColor.graySpace


}
}

这里有一个简化的、更具表现力的解决方案。它既适用于值类型,也适用于引用类型。举重的方法取自@HepaKKes 的回答。

联系代码:

import ObjectiveC


final class Lifted<T> {
let value: T
init(_ x: T) {
value = x
}
}


private func lift<T>(_ x: T) -> Lifted<T>  {
return Lifted(x)
}


func associated<T>(to base: AnyObject,
key: UnsafePointer<UInt8>,
policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN,
initialiser: () -> T) -> T {
if let v = objc_getAssociatedObject(base, key) as? T {
return v
}


if let v = objc_getAssociatedObject(base, key) as? Lifted<T> {
return v.value
}


let lifted = Lifted(initialiser())
objc_setAssociatedObject(base, key, lifted, policy)
return lifted.value
}


func associate<T>(to base: AnyObject, key: UnsafePointer<UInt8>, value: T, policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN) {
if let v: AnyObject = value as AnyObject? {
objc_setAssociatedObject(base, key, v, policy)
}
else {
objc_setAssociatedObject(base, key, lift(value), policy)
}
}

用法示例:

1)创建扩展并将属性关联到它。让我们同时使用值和引用类型属性。

extension UIButton {


struct Keys {
static fileprivate var color: UInt8 = 0
static fileprivate var index: UInt8 = 0
}


var color: UIColor {
get {
return associated(to: self, key: &Keys.color) { .green }
}
set {
associate(to: self, key: &Keys.color, value: newValue)
}
}


var index: Int {
get {
return associated(to: self, key: &Keys.index) { -1 }
}
set {
associate(to: self, key: &Keys.index, value: newValue)
}
}


}

2)现在你可以像使用常规属性一样使用:

    let button = UIButton()
print(button.color) // UIExtendedSRGBColorSpace 0 1 0 1 == green
button.color = .black
print(button.color) // UIExtendedGrayColorSpace 0 1 == black


print(button.index) // -1
button.index = 3
print(button.index) // 3

更多细节:

  1. 包装值类型需要提升。
  2. 保留默认的关联对象行为。如果你想更多地了解关联对象,我建议检查 这篇文章

关联对象 API 使用起来有点麻烦。您可以使用助手类删除大部分样板。

public final class ObjectAssociation<T: AnyObject> {


private let policy: objc_AssociationPolicy


/// - Parameter policy: An association policy that will be used when linking objects.
public init(policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {


self.policy = policy
}


/// Accesses associated object.
/// - Parameter index: An object whose associated object is to be accessed.
public subscript(index: AnyObject) -> T? {


get { return objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? }
set { objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy) }
}
}

假设你可以以一种更易读的方式“添加”一个属性到 Objective-c 类中:

extension SomeType {


private static let association = ObjectAssociation<NSObject>()


var simulatedProperty: NSObject? {


get { return SomeType.association[self] }
set { SomeType.association[self] = newValue }
}
}

至于解决方案:

extension CALayer {


private static let initialPathAssociation = ObjectAssociation<CGPath>()
private static let shapeLayerAssociation = ObjectAssociation<CAShapeLayer>()


var initialPath: CGPath! {
get { return CALayer.initialPathAssociation[self] }
set { CALayer.initialPathAssociation[self] = newValue }
}


var shapeLayer: CAShapeLayer? {
get { return CALayer.shapeLayerAssociation[self] }
set { CALayer.shapeLayerAssociation[self] = newValue }
}
}

这里有一个同样有效的替代方案

public final class Storage : AnyObject {


var object:Any?


public init(_ object:Any) {
self.object = object
}
}


extension Date {


private static let associationMap = NSMapTable<NSString, AnyObject>()
private struct Keys {
static var Locale:NSString = "locale"
}


public var locale:Locale? {
get {


if let storage = Date.associationMap.object(forKey: Keys.Locale) {
return (storage as! Storage).object as? Locale
}
return nil
}
set {
if newValue != nil {
Date.associationMap.setObject(Storage(newValue), forKey: Keys.Locale)
}
}
}
}






var date = Date()
date.locale = Locale(identifier: "pt_BR")
print( date.locale )

使用 Objective-C 相关对象和 Swift 3Swift 4的计算属性的另一个示例

import CoreLocation


extension CLLocation {


private struct AssociatedKeys {
static var originAddress = "originAddress"
static var destinationAddress = "destinationAddress"
}


var originAddress: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.originAddress) as? String
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.originAddress,
newValue as NSString?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}


var destinationAddress: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.destinationAddress) as? String
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.destinationAddress,
newValue as NSString?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}


}

为什么要依赖 Objecc 运行时?我不明白。通过使用下面这样的方法,您将实现与存储属性几乎相同的行为,只需使用 完全迅速的方法:

extension UIViewController {
private static var _myComputedProperty = [String:Bool]()


var myComputedProperty:Bool {
get {
let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
return UIViewController._myComputedProperty[tmpAddress] ?? false
}
set(newValue) {
let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
UIViewController._myComputedProperty[tmpAddress] = newValue
}
}
}

如果您正在寻找设置一个自定义字符串属性到 UIView,这是我如何做到这一点在 Swift 4

创建 UIView 扩展

extension UIView {


func setStringValue(value: String, key: String) {
layer.setValue(value, forKey: key)
}


func stringValueFor(key: String) -> String? {
return layer.value(forKey: key) as? String
}
}

使用此扩展插件

let key = "COLOR"


let redView = UIView()


// To set
redView.setStringAttribute(value: "Red", key: key)


// To read
print(redView.stringValueFor(key: key)) // Optional("Red")

注意: 在进一步分析之后,下面的代码工作得很好,但是没有释放视图对象,所以如果我能找到一种方法绕过它,我将编辑答案。同时,请阅读评论。

如何存储静态映射到类,扩展如下:

extension UIView {
    

struct Holder {
static var _padding:[UIView:UIEdgeInsets] = [:]
}
   

var padding : UIEdgeInsets {
get{ return UIView.Holder._padding[self] ?? .zero}
set { UIView.Holder._padding[self] = newValue }
}


}

绝对迅捷软弱参考处理

import Foundation
import UIKit


extension CustomView {
    

// can make private
static let storedProperties = WeakDictionary<UIView, Properties>()
    

struct Properties {
var url: String = ""
var status = false
var desc: String { "url: \(url), status: \(status)" }
}
    

var properties: Properties {
get {
return CustomView.storedProperties.get(forKey: self) ?? Properties()
}
set {
CustomView.storedProperties.set(forKey: self, object: newValue)
}
}
}


var view: CustomView? = CustomView()
print("1 print", view?.properties.desc ?? "nil")
view?.properties.url = "abc"
view?.properties.status = true
print("2 print", view?.properties.desc ?? "nil")
view = nil

弱字典,迅捷

import Foundation


private class WeakHolder<T: AnyObject>: Hashable {
weak var object: T?
let hash: Int


init(object: T) {
self.object = object
hash = ObjectIdentifier(object).hashValue
}


func hash(into hasher: inout Hasher) {
hasher.combine(hash)
}


static func ==(lhs: WeakHolder, rhs: WeakHolder) -> Bool {
return lhs.hash == rhs.hash
}
}


class WeakDictionary<T1: AnyObject, T2> {
private var dictionary = [WeakHolder<T1>: T2]()


func set(forKey: T1, object: T2?) {
dictionary[WeakHolder(object: forKey)] = object
}


func get(forKey: T1) -> T2? {
let obj = dictionary[WeakHolder(object: forKey)]
return obj
}


func forEach(_ handler: ((key: T1, value: T2)) -> Void) {
dictionary.forEach {
if let object = $0.key.object, let value = dictionary[$0.key] {
handler((object, value))
}
}
}
    

func clean() {
var removeList = [WeakHolder<T1>]()
dictionary.forEach {
if $0.key.object == nil {
removeList.append($0.key)
}
}
removeList.forEach {
dictionary[$0] = nil
}
}
}

首先,相关对象应该是扩展存储属性的最佳解决方案,因为它来自 Objective-C 运行时,这是一个非常强大的特性,我们应该使用 在斯威夫特语还有其他母语特征之前

你应该总是意识到相关对象将被释放后,没有其他对象保留它们,包括快速对象,所以不要使用自定义容器保留目标值,这将不会自动释放。

其次,对于那些额外的相关键结构定义,核心函数只需要一个 UnsafeRawPointer,实际上还有另一个最佳选择,#function是一个静态字符串,在编译源代码时生成,它也有自己的地址使用。

就是这样:

var status: Bool? {
get { objc_getAssociatedObject(self, #function) as? Bool }
set { objc_setAssociatedObject(self, #function, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)}
}

为迅速5建造。

最后,请记住关联策略的对象类型。

为什么不只是做这样的事情,我看到其他的解决方案是出于小的需要。

private var optionalID: String {
UUID().uuidString
}