Comment avoir des propriétés stockées dans Swift, de la même manière que J'avais sur Objective-C?

Je passe d'une application D'Objective-C à Swift, que j'ai quelques catégories avec des propriétés stockées, par exemple:

@interface UIView (MyCategory)

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

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

@end

Comme les extensions Swift n'acceptent pas de telles propriétés stockées, Je ne sais pas comment maintenir la même structure que le code Objc. Les propriétés stockées sont vraiment importantes pour mon application et je crois Qu'Apple a dû créer une solution pour le faire dans Swift.

Comme dit par jou, ce que je cherchais était en fait en utilisant associated objets, donc je l'ai fait (dans un autre contexte):

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))
        }
    }
}

Mais j'obtiens un EXC_BAD_ACCESS en faisant:

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

Des idées?

123
demandé sur Koen. 2014-08-21 16:48:42

16 réponses

L'API objets associés est un peu encombrante à utiliser. Vous pouvez supprimer la plupart de la passe-partout avec une classe d'aide.

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) }
    }
}

À condition que vous puissiez "ajouter" une propriété à la classe objective-c d'une manière plus lisible:

extension SomeType {

    private static let association = ObjectAssociation<NSObject>()

    var simulatedProperty: NSObject? {

        get { return SomeType.association[self] }
        set { SomeType.association[self] = newValue }
    }
}
42
répondu Wojciech Nagrodzki 2017-03-27 20:39:00

Comme dans Objective-C, vous ne pouvez pas ajouter de propriété stockée aux classes existantes. Si vous étendez une classe Objective-C ({[2] } en est certainement une), vous pouvez toujours utiliser objets associés pour émuler les propriétés stockées:

Pour 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))
        }
    }
}

La clé d'association est un pointeur qui devrait être l'unique pour chaque association. Pour cela, nous créons une variable globale privée et utilisons son adresse mémoire comme clé avec l'opérateur &. Voir le en utilisant Swift avec Cacao et Objective-C sur plus de détails comment les pointeurs sont gérés dans Swift.

Mise à jour pour Swift 2 et 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)
        }
    }
}
177
répondu jou 2016-11-28 05:34:45

Donc, je pense avoir trouvé une méthode qui fonctionne plus propre que celles ci-dessus car elle ne nécessite aucune variable globale. Je l'ai eu à partir d'ici: http://nshipster.com/swift-objc-runtime/

L'essentiel est que vous utilisez une structure comme ceci:

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)
                )
            }
        }
    }
}

Mise à JOUR pour 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)
    }
}
34
répondu AlexK 2015-11-16 13:33:11

La solution pointée par jou ne prend pas en charge les types de valeur , cela fonctionne très bien avec eux

Emballages

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
    }
}

A possible extension de Classe (Exemple d'utilisation):

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)
            }
        }
    }
}

C'est vraiment une excellente solution, je voulais ajouter un autre exemple d'utilisation qui incluait des structures et des valeurs qui ne sont pas optionnelles. En outre, les valeurs AssociatedKey peuvent être simplifiées.

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)
        }
    }
}
14
répondu HepaKKes 2016-02-06 23:20:16

Vous ne pouvez pas définir de catégories (extensions Swift) avec un nouveau stockage; toutes les propriétés supplémentaires doivent être calculées plutôt que stockées. La syntaxe fonctionne pour Objective C car @property dans une catégorie signifie essentiellement "je vais fournir le getter et le setter". Dans Swift, vous devrez les définir vous-même pour obtenir une propriété calculée; quelque chose comme:

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

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

Devrait fonctionner correctement. Rappelez-vous, vous ne pouvez pas stocker de nouvelles valeurs dans le setter, ne fonctionnent qu'avec l'état de classe disponible existant.

10
répondu Adam Wright 2016-03-04 18:26:46

Mon $ 0.02. Ce code est écrit dans 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)
            }
        }
    }
}

J'ai essayé de nombreuses solutions, et j'ai trouvé que c'était le seul moyen d'étendre une classe avec des paramètres variables supplémentaires.

7
répondu Amro Shafie 2015-08-27 15:41:26

Je préfère faire du code dans Pure Swift et ne pas compter sur L'héritage Objective-C. Pour cette raison, j'ai écrit une solution rapide pure avec deux avantages et deux inconvénients.

Avantages:

  1. Code Swift pur

  2. Fonctionne sur les classes et les complétions ou plus spécifiquement sur Any object

Inconvénients:

  1. Le Code doit appeler la méthode willDeinit() pour libérer les objets liés à une instance de classe spécifique à éviter les fuites de mémoire

  2. Vous ne pouvez pas faire d'extension directement à UIView pour cet exemple exact car var frame est une extension à UIView, ne faisant pas partie de la classe.

Modifier:

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
    }
}
5
répondu vedrano 2016-12-08 05:13:06

Pourquoi compter sur objc runtime? Je ne comprends pas le point. En utilisant quelque chose comme ce qui suit, vous obtiendrez presque le comportement identique d'une propriété stockée, en utilisant uniquement une approche Pure Swift :

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
        }
    }
}
5
répondu valvoline 2018-03-29 21:55:39

Avec les catégories Obj-c, vous ne pouvez ajouter que des méthodes, pas des variables d'instance.

Dans votre exemple, vous avez utilisé @property comme raccourci pour ajouter des déclarations de méthodes getter et setter. Vous devez toujours implémenter ces méthodes.

De même dans Swift, vous pouvez ajouter des extensions d'utilisation pour ajouter des méthodes d'instance, des propriétés calculées, etc. mais pas les propriétés stockées.

2
répondu Mike Pollard 2014-08-21 13:01:50

J'ai aussi un problème EXC_BAD_ACCESS.La valeur dans objc_getAssociatedObject() et objc_setAssociatedObject() doit être un Objet. Et le {[2] } doit correspondre à l'objet.

2
répondu RuiKQ 2016-02-15 16:18:47

J'ai essayé d'utiliser objc_setAssociatedObject comme mentionné dans quelques-unes des réponses ici, mais après avoir échoué plusieurs fois, je me suis retiré et j'ai réalisé qu'il n'y avait aucune raison d'en avoir besoin. Empruntant à quelques-unes des idées ici, j'ai trouvé ce code qui stocke simplement un tableau de mes données supplémentaires (MyClass dans cet exemple) indexé par l'objet auquel je veux l'associer:

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)
2
répondu Dan 2016-08-08 01:14:01

Voici une solution simplifiée et plus expressive. Cela fonctionne à la fois pour les types de valeur et de référence. L'approche de levage est tirée de la réponse @ HepaKKes.

Code D'Association:

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)
    }
}

Exemple d'utilisation:

1) créez une extension et associez-lui des propriétés. Utilisons les propriétés value et reference type.

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) Maintenant, vous pouvez utiliser des propriétés aussi régulières:

    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

Plus de détails:

  1. le levage est nécessaire pour envelopper les types de valeur.
  2. le comportement par défaut de l'objet associé est retain. Si vous voulez en savoir plus sur les objets associés, je vous recommande de vérifier cet article.
2
répondu Vadim Bulavin 2017-02-28 13:20:15

Un autre exemple avec L'utilisation D'objets associés à Objective-C et de propriétés calculées pour Swift 3 et Swift 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
                )
            }
        }
    }

}
1
répondu abdullahselek 2017-09-20 13:30:00

J'ai essayé de stocker les propriétés en utilisant objc_getAssociatedObject, objc_setAssociatedObject, sans aucune chance. Mon objectif était de créer une extension pour UITextField, pour valider la longueur des caractères d'entrée de texte. Le code suivant fonctionne bien pour moi. Espérons que cela aidera quelqu'un.

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)
    }
}
0
répondu Vadims Krutovs 2016-12-02 09:27:43

Voici une alternative qui fonctionne aussi

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 )
0
répondu Rondinelli Morais 2017-06-23 18:06:51

J'ai trouvé cette solution plus pratique

mise à jour pour 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)
    }

}

...puis dans votre code

class gameController: UIViewController {

    @IBOutlet var game: gameClass!

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

    }
}
-1
répondu Giuseppe Mazzilli 2017-01-25 14:39:12