How to have stored properties in Swift, the same way I had on Objective-C?
Asked Answered
Z

22

145

I am switching an application from Objective-C to Swift, which I have a couple of categories with stored properties, for example:

@interface UIView (MyCategory)

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

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

@end

As Swift extensions don't accept stored properties like these, I don't know how to maintain the same structure as the Objc code. Stored properties are really important for my app and I believe Apple must have created some solution for doing it in Swift.

As said by jou, what I was looking for was actually using associated objects, so I did (in another context):

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

But I get an EXC_BAD_ACCESS when doing:

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

Any ideas?

Zombie answered 21/8, 2014 at 12:48 Comment(2)
Objective-C class categories can't define instance variables either, so how did you realize those properties?Balch
Not sure why you get a bad access, but your code should not work. You are passing different values to setAssociateObject and getAssociatedObject. Using the string "shapeLayer" as a key is wrong, it's the pointer (it's address actually) that is the key, not what it points to. Two identical strings residing at different addresses are two different keys. Review Jou's answer and notice how he defined xoAssociationKey to be a global variable, so it is the same key/pointer when setting/getting.Coincide
W
60

Associated objects API is a bit cumbersome to use. You can remove most of the boilerplate with a helper class.

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

Provided that you can "add" a property to objective-c class in a more readable manner:

extension SomeType {

    private static let association = ObjectAssociation<NSObject>()

    var simulatedProperty: NSObject? {

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

As for the solution:

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 }
    }
}
Wunder answered 27/3, 2017 at 20:39 Comment(12)
Ok what to do if I want to store Int, Bool and etc?Contorted
It is not possible to store Swift types via object association directly. You could store e.g. NSNumber or NSValue and write additional pair of accessors that would be of the types you wanted (Int, Bool, etc).Wunder
WARNING This does not work for structs, because the objective-c runtime library only supports classes that conform to NSObjectProtocolCanoe
@CharltonProvatas It is not possible to use structs with this API, they do not conform to AnyObject protocol.Wunder
@VyachaslavGerchicov [String]? is a struct, this is the same case as for Int, Bool. You could use NSArray to keep a collection of NSString instances.Wunder
@VyachaslavGerchicov I am not sure how I can make this any clearer - I am sorry. You can use any class that inherits from NSObject to specialize generic ObjectAssociation. If you want to store CAShapeLayer use ObjectAssociation<CAShapeLayer> .Wunder
Sorry I didn't understand your comment previously. Yes, CAShapeLayer is NSObject so it could be used with your code. No, CGPathRef! is structure which couldn't be used with your code. Finally yes, [String] is structure, No, [String]? is not structure - it is enum. A correct and universal code exists + your answer is marked as "best" but you didn't answer the question so downvote mark remainsContorted
@VyachaslavGerchicov Yes [String]? is enum, what I head in mind it is a value type which does not conform to AnyObject. I added the solution. I hope this helps.Wunder
I'm trying to use this one for an array of objects [anyObject] but I can't make it work. Does this work for arrays of objects too?Bladdernose
@JoaquinPereira Try keeping objects in NSArray via ObjectAssociation<NSArray>Wunder
Works like a charm for classes, but unfortunately does not work for structures, because they're not objects. But for structs like String, you can make an extension for NSString and easily cast it back to String. There's no solution for custom structChem
crashes if you call get before setMartian
E
194

As in Objective-C, you can't add stored property to existing classes. If you're extending an Objective-C class (UIView is definitely one), you can still use Associated Objects to emulate stored properties:

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

The association key is a pointer that should be the unique for each association. For that, we create a private global variable and use it's memory address as the key with the & operator. See the Using Swift with Cocoa and Objective-C on more details how pointers are handled in Swift.

UPDATED for Swift 2 and 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)
        }
    }
}
Erysipeloid answered 21/8, 2014 at 13:46 Comment(23)
That's exactly what I was looking for, but I still have problems. In my CALayer extension I have a property called shapeLayer:CAShapeLayer! with a getter and setter just like you suggested, but in my UIView subclass whenever try doing self.layer.shapeLayer I get a EXC_BAD_ACCESS error. Any suggestions? CheersZombie
Where does shapeLayer gets set? Could it be that it wasn't set before you read it and it's the result of trying to use a nil value?Erysipeloid
Unfortunately it's not it, I get the error when doing self.layer.shapeLayer = CAShapeLayer()Zombie
I just tried the same code above but extending CALayer with a shapeLayer property and it worked fine. Could you update your question with the code for your CALayer extension?Erysipeloid
The key in objc_getAssociatedObject() and objc_setAssociatedObject() must be a pointer, not a String. See the new paragraph I addedErysipeloid
Thanks, great! For clarity, I would use objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN) instead of UInt(OBJC_ASSOCIATION_RETAIN)...Kletter
@Yar Didn't know about objc_AssociationPolicy, thanks! I've updated the answerErysipeloid
Glad to help! Here's my full blog article, which is essentially more of the same: yar2050.com/2015/01/associated-objects-in-swift.htmlKletter
This doesn't work with value types. String, Int, Double, etc. are automatically bridged, but this solution doesn't work in general with structsIngold
This should be rewarded 50 points! I was pulling my hair trying to figre out how I can store a String in a UIView extension. You saved me!Soelch
In Swift2 you have to use objc_AssociationPolicy.OBJC_ASSOCIATION_RETAINImmunotherapy
@SimplGy please add your edit as an additional answer rather than editing your code into someone else's answer.Cirri
It should be mentioned in this answer that this tightly couples you to the dynamic features of the underlying Objective-C implementation. This will not work in non-ObjC based implementations of Swift.Hornback
@SimplGy Can you change the 3.0 code snippet to point out the differences from 2.2? I can't tell what you're doing differently...Bidentate
@BenLeggiero I just removed it. I think the type is different (Int), but it breaks in Swift 3 edge (xcode beta) anyway so I couldn't do a meaningful test on it.Junette
only difference b/w Swift 2 & 3 is : OBJC_ASSOCIATION_RETAIN ?Siberson
It works but it creates HUGE LEAKS, especially if you use your properties in a TableViewCell !Portal
I just did some testing with Instruments and there was no memory leak unlike @Portal said. It might have been caused by your UITableView implementation at that time.Conchiferous
Swift4 solution doesn't work if you want to have more than 1 instance of UIViewController that uses the property from extension. Please check updates in source.Glottology
@Erysipeloid Why is it upvoted?! It doesn't work! I wanted to store [String]?. The result is I have a single variable not multiple variables stored in different classes. Even If you look at this "soltuion" - In swift 4 it recommends to save the variable directly in the static variable which is single of course! In general - if you use just a static variable then why not to use it directly without of these uselss wrappers?Contorted
The Swift 4 example doesn't work with multiple instances, and probably should not be here.Dailey
Doesn't work with multiple instancesChem
I removed the bad Swift 4 example, 100% should not be thereMartian
W
60

Associated objects API is a bit cumbersome to use. You can remove most of the boilerplate with a helper class.

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

Provided that you can "add" a property to objective-c class in a more readable manner:

extension SomeType {

    private static let association = ObjectAssociation<NSObject>()

    var simulatedProperty: NSObject? {

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

As for the solution:

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 }
    }
}
Wunder answered 27/3, 2017 at 20:39 Comment(12)
Ok what to do if I want to store Int, Bool and etc?Contorted
It is not possible to store Swift types via object association directly. You could store e.g. NSNumber or NSValue and write additional pair of accessors that would be of the types you wanted (Int, Bool, etc).Wunder
WARNING This does not work for structs, because the objective-c runtime library only supports classes that conform to NSObjectProtocolCanoe
@CharltonProvatas It is not possible to use structs with this API, they do not conform to AnyObject protocol.Wunder
@VyachaslavGerchicov [String]? is a struct, this is the same case as for Int, Bool. You could use NSArray to keep a collection of NSString instances.Wunder
@VyachaslavGerchicov I am not sure how I can make this any clearer - I am sorry. You can use any class that inherits from NSObject to specialize generic ObjectAssociation. If you want to store CAShapeLayer use ObjectAssociation<CAShapeLayer> .Wunder
Sorry I didn't understand your comment previously. Yes, CAShapeLayer is NSObject so it could be used with your code. No, CGPathRef! is structure which couldn't be used with your code. Finally yes, [String] is structure, No, [String]? is not structure - it is enum. A correct and universal code exists + your answer is marked as "best" but you didn't answer the question so downvote mark remainsContorted
@VyachaslavGerchicov Yes [String]? is enum, what I head in mind it is a value type which does not conform to AnyObject. I added the solution. I hope this helps.Wunder
I'm trying to use this one for an array of objects [anyObject] but I can't make it work. Does this work for arrays of objects too?Bladdernose
@JoaquinPereira Try keeping objects in NSArray via ObjectAssociation<NSArray>Wunder
Works like a charm for classes, but unfortunately does not work for structures, because they're not objects. But for structs like String, you can make an extension for NSString and easily cast it back to String. There's no solution for custom structChem
crashes if you call get before setMartian
D
38

So I think I found a method that works cleaner than the ones above because it doesn't require any global variables. I got it from here: http://nshipster.com/swift-objc-runtime/

The gist is that you use a struct like so:

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

UPDATE for 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)
    }
}
Diagenesis answered 17/8, 2015 at 23:18 Comment(5)
@AKWeblS I have implemented code like yours but when updating to swift 2.0, i am getting the error cannot invoke initializer for type 'UInt' with an argument list of type 'objc_AssociationPolicy)'. Code in next commentHospitalet
How to update for swift 2.0? var postDescription: String? { get { return objc_getAssociatedObject(self, &AssociatedKeys.postDescription) as? String } set { if let newValue = newValue { objc_setAssociatedObject( self, &AssociatedKeys.postDescription, newValue as NSString?, UInt(objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) ) } } }Hospitalet
doesn't work : static var means that the value of the property is the same for all instanceValediction
@fry - it continues to work for me. Yes, the key is static, but its associated with a specific object, the object itself is not global.Diagenesis
This is so sexy! Thanks so much!Fawnia
I
18

The solution pointed out by jou doesn't support value types, this works fine with them as well

Wrappers

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 Class extension (Example of usage):

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

This is really such a great solution, I wanted to add another usage example that included structs and values that are not optionals. Also, the AssociatedKey values can be simplified.

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)
        }
    }
}
Ingold answered 15/4, 2015 at 22:50 Comment(5)
And how do I use the Lifted class?Lyndell
Why do you need that? What for?Ingold
I meant how do you use this in general, thanks for your help :) Can you add a "How to use" example, please?Lyndell
The "How to use" example would start right below "A possible Class extension" header. I don't think users need to know how to use the lift method and the Lifted class, they should use however getAssociatedObject & setAssociatedObject functions. I'll add "Example of usage" between parenthesis next to the header for the sake of clarity.Ingold
This is certainly the best solution, too bad it is not explained very well. It works with classes, structs, and other value types. The properties do not have to be optionals either. Nice work.Goy
P
13

You can't define categories (Swift extensions) with new storage; any additional properties must be computed rather than stored. The syntax works for Objective C because @property in a category essentially means "I'll provide the getter and setter". In Swift, you'll need to define these yourself to get a computed property; something like:

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

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

Should work fine. Remember, you can't store new values in the setter, only work with the existing available class state.

Plumbism answered 21/8, 2014 at 12:56 Comment(0)
O
7

My $0.02. This code is written in 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)
            }
        }
    }
}

I have tried many solutions, and found this is the only way to actually extend a class with extra variable parameters.

Osculation answered 27/8, 2015 at 15:35 Comment(1)
Looks interesting, but refuses doesn't work with protocols, type 'AssociatedKeys' cannot be defined within a protocol extensionZoan
C
6

Why relying on objc runtime? I don't get the point. By using something like the following you will achieve almost the identical behaviour of a stored property, by using only a pure Swift approach:

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
        }
    }
}
Conceptionconceptual answered 28/3, 2018 at 21:8 Comment(2)
Clever! One possible downside with this approach is memory management. After the host object is deallocated, it's property will still exist in the dictionary which could potentially get expensive on memory usage if this is utilized with a lot of objects.Canoe
Probably we can hold a static list variable of wrappers which contain weak reference to the objects(self). And remove entry from _myComputedProperty dictionary as well static list variable of wrappers in the didSet method of Wrapper(When the object gets reallocated didSet inside our Wrapper class gets called since our weak reference is set a new value with nil).Disrespectable
D
5

I prefer doing code in pure Swift and not rely on Objective-C heritage. Because of this I wrote pure Swift solution with two advantages and two disadvantages.

Advantages:

  1. Pure Swift code

  2. Works on classes and completions or more specifically on Any object

Disadvantages:

  1. Code should call method willDeinit() to release objects linked to specific class instance to avoid memory leaks

  2. You cannot make extension directly to UIView for this exact example because var frame is extension to UIView, not part of class.

EDIT:

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
    }
}
Describe answered 22/7, 2015 at 16:31 Comment(11)
This does not work since extensionPropertyStorage is shared among all instances. If you set a value for one instance, you are setting the value for all instances.Goy
It war not good because messing up with functions. Now it is better and works as intended.Describe
@Goy extensionPropertyStorage is shared with all instances by design. It is global variable (Dictionary) that first de-references UILabel instance (NSObject) and then its property ([String: Any]).Describe
@Goy I guess this works for class properties ;)Hypethral
frame is an instance property. Where does class property come from?Describe
The idea is good, much better then using objective-c associated objects. This is the best solution, but suggested implementation is not production ready.Extinct
@Describe do you have any idea how to automatically clear extraData so it doesn't cause memory leaks?Stephaniestephannie
@Stephaniestephannie I would say that you can add the following code: deinit { willDeinit() } so app will make cleanup as soon as UILabel gets released.Describe
Actually for this to work you should rewrite code to use NSMapTable because this implementation with strong links would never call deinit(). Other problem would be that original UILabel.deinit() would never be called.Describe
@Describe , I called willDeinit() inside viewWillDisapper instead of deinit and it works fineStephaniestephannie
@DoubleK, yes, that is a nice trick. You loose some generality but it should work just fine for UIView subclass in UIViewController.Describe
S
3

With Obj-c Categories you can only add methods, not instance variables.

In you example you have used @property as a shortcut to adding getter and setter method declarations. You still need to implement those methods.

Similarly in Swift you can add use extensions to add instance methods, computed properties etc. but not stored properties.

Schargel answered 21/8, 2014 at 13:1 Comment(0)
M
3

First, Associated Objects should be the best right solution for the extended stored properties, because it comes from the Objective-C runtime, this is a great powerful feature that we should use before there are other native features of Swift language.

You should always aware that the associated objects will be released after there are no other objects to retain them, including swift objects, so don't use custom containers to retain the target values which won't be released automatically.

Second, for those additional associated key structure definitions, the core functions just need a UnsafeRawPointer for that, actually there is another best choice for that, #function is a static string which generated when compiling the source code, it also has its own address to use.

So, here is it:

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

Build for swift 5.

Last, keep in mind the object type with the association policy.

Mini answered 11/3, 2022 at 4:0 Comment(0)
V
2

I also get an EXC_BAD_ACCESS problem.The value in objc_getAssociatedObject() and objc_setAssociatedObject() should be an Object. And the objc_AssociationPolicy should match the Object.

Vestryman answered 15/2, 2016 at 8:26 Comment(2)
This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. You can also add a bounty to draw more attention to this question once you have enough reputation. - From ReviewBriefing
@Briefing I am so sorry about that. I try to add a comment to the question.But I do not have enough reputation. So I try to answer the question.Vestryman
C
2

I tried using objc_setAssociatedObject as mentioned in a few of the answers here, but after failing with it a few times I stepped back and realized there is no reason I need that. Borrowing from a few of the ideas here, I came up with this code which simply stores an array of whatever my extra data is (MyClass in this example) indexed by the object I want to associate it with:

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)
Curd answered 8/8, 2016 at 1:14 Comment(4)
do you have any idea how to automatically clear extraData so it doesn't cause memory leaks?Stephaniestephannie
I wasn't actually using this with views, in my case I only ever instantiate a finite number of objects in the class I was using, so I haven't really considered memory leaks. I can't think of an automatic way to do this, but I suppose in the setter above you could add logic to remove the element if value == nil, and then set the value to nil when you no longer need the object, or maybe in didReceiveMemoryWarning or something.Curd
Tnx @Dan, I used viewWillDisapper to set value to nil and it works fine, now deinit is called and everything works fine. Tnx for posting this solutionStephaniestephannie
DO NOT try to override parent methods in extension.Mini
A
2

Here is simplified and more expressive solution. It works for both value and reference types. The approach of lifting is taken from @HepaKKes answer.

Association code:

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

Example of usage:

1) Create extension and associate properties to it. Let's use both value and reference type properties.

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) Now you can use just as regular properties:

    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

More details:

  1. Lifting is needed for wrapping value types.
  2. Default associated object behavior is retain. If you want to learn more about associated objects, I'd recommend checking this article.
Astral answered 26/1, 2017 at 9:22 Comment(0)
S
2

Notice: after further analyzing, the code below works fine, but does not release the view object, so if I can find a way around it I'll edit the answer. meanwhile, read the comments.

How about storing static map to class that is extending like this :

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

}
Stenophyllous answered 24/6, 2019 at 7:22 Comment(3)
why this answer has no upvotes. Smart solution after all.Labyrinthine
This works but I think this approach is flawed as stated here medium.com/@valv0/…Mesh
Downvote! After _padding retains the incoming view object in its key, when will the view be released?Mini
A
1

Another example with using Objective-C associated objects and computed properties for Swift 3 and 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
                )
            }
        }
    }

}
Africa answered 21/8, 2017 at 13:32 Comment(0)
E
1

if you are looking to set a custom string attribute to a UIView, this is how I did it on Swift 4

Create a UIView extension

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

To use this extension

let key = "COLOR"

let redView = UIView() 

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

// To read
print(redView.stringValueFor(key: key)) // Optional("Red")
Evelineevelinn answered 30/1, 2019 at 4:33 Comment(1)
The best part about storing your junk in CALayer that it will probably archive it too LOL. You can verify that with layer.shouldArchiveValue(forKey: "COLOR") // true. Just don't do it man.Neurogenic
T
1

In PURE SWIFT with WEAK reference handling

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

WeakDictionary.swift

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
        }
    }
}
Trudytrue answered 12/11, 2020 at 12:34 Comment(2)
This is also mentioned in this article valv0.medium.com/…Hind
The article doesn't cover memory leaking.Trudytrue
W
0

I tried to store properties by using objc_getAssociatedObject, objc_setAssociatedObject, without any luck. My goal was create extension for UITextField, to validate text input characters length. Following code works fine for me. Hope this will help someone.

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)
    }
}
Wouldst answered 2/12, 2016 at 9:27 Comment(2)
Global private variables? Do you realize that _min and _max are global and will be the same in all instances of the UITextField? Even if it works for you, this answer is unrelated because Marcos asking about instance variables.Emersonemery
Yes you right, this not working for all the instances. I Fixed by storing min and max value in struct. Sorry for off topic.Wouldst
P
0

Why not just do something like this, i see other solutions are way out of the small need.

private var optionalID: String {
    UUID().uuidString
}
Palfrey answered 21/6, 2022 at 10:59 Comment(0)
E
-1

Here is an alternative that works also

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 )
Eyeleteer answered 23/6, 2017 at 18:6 Comment(1)
Not related much to the original question. This 'associates' on a key "locale", not the instance of the extended class. If you create another instance of Date; setting local will change the value on all instances. An NSMapTable (with a weak key) would be a potential solution to this problem tho.Voyeurism
P
-1

this sample code works for me, and fix memory leak.

SWIFT 5.2 (iOS 14+)

(tested in playground, you can copy/paste for test)

class ExampleClass {}

class OtherClass {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("instance \(name) deinit")
    }
}

fileprivate struct ExampleClassAssociatedKeys {
    static var otherClass: UInt8 = 0
}
extension ExampleClass {
    var storedProperty: OtherClass? {
        get {
            return objc_getAssociatedObject(self, &ExampleClassAssociatedKeys.otherClass) as? OtherClass
        }
        set {
            objc_setAssociatedObject(self, &ExampleClassAssociatedKeys.otherClass, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

var a: ExampleClass? = ExampleClass()
var b: ExampleClass? = ExampleClass()
print(a?.storedProperty) //nil
b?.storedProperty = OtherClass(name: "coucou")
a?.storedProperty = OtherClass(name: "blabla")
print(a?.storedProperty?.name) // Optional("blabla")
print(b?.storedProperty?.name) // Optional("coucou")
a = nil //instance blabla deinit
b = nil //instance coucou deinit
Proclus answered 15/6, 2023 at 7:16 Comment(0)
S
-2

I found this solution more practical

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

}

...then in your code

class gameController: UIViewController {

    @IBOutlet var game: gameClass!

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

    }
}
Sundry answered 25/1, 2017 at 14:32 Comment(1)
This is the right way to do it - but not really a stored property in the extension like the question asks.Sweeny

© 2022 - 2024 — McMap. All rights reserved.