144

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?

Koen.
  • 25,449
  • 7
  • 83
  • 78
Marcos Duarte
  • 3,472
  • 4
  • 19
  • 22
  • 14
    Objective-C class categories can't define instance variables either, so how did you realize those properties? – Martin R Aug 21 '14 at 12:55
  • 1
    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. – SafeFastExpressive Oct 08 '14 at 19:15

22 Answers22

192

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

UPDATED for Swift 4

In Swift 4, it's much more simple. The Holder struct will contain the private value that our computed property will expose to the world, giving the illusion of a stored property behaviour instead.

Source

extension UIViewController {
    struct Holder {
        static var _myComputedProperty:Bool = false
    }
    var myComputedProperty:Bool {
        get {
            return Holder._myComputedProperty
        }
        set(newValue) {
            Holder._myComputedProperty = newValue
        }
    }
}
Romulo BM
  • 671
  • 8
  • 16
jou
  • 5,824
  • 3
  • 22
  • 24
  • 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? Cheers – Marcos Duarte Aug 21 '14 at 14:35
  • 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? – jou Aug 21 '14 at 20:03
  • Unfortunately it's not it, I get the error when doing `self.layer.shapeLayer = CAShapeLayer()` – Marcos Duarte Aug 21 '14 at 23:35
  • 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? – jou Aug 22 '14 at 08:02
  • The key in `objc_getAssociatedObject()` and `objc_setAssociatedObject()` must be a pointer, not a String. See the new paragraph I added – jou Aug 25 '14 at 07:24
  • Thanks, great! For clarity, I would use `objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN)` instead of `UInt(OBJC_ASSOCIATION_RETAIN)`... – Dan Rosenstark Jan 21 '15 at 15:24
  • 2
    @Yar Didn't know about `objc_AssociationPolicy`, thanks! I've updated the answer – jou Jan 22 '15 at 20:42
  • Glad to help! Here's my full blog article, which is essentially more of the same: http://yar2050.com/2015/01/associated-objects-in-swift.html – Dan Rosenstark Jan 22 '15 at 21:46
  • This doesn't work with value types. *String, Int, Double, etc.* are automatically bridged, but this solution doesn't work in general with **structs** – HepaKKes Apr 16 '15 at 11:25
  • 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! – user1366265 May 15 '15 at 12:45
  • 2
    In Swift2 you have to use objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN – Tom Oct 27 '15 at 20:42
  • 1
    @SimplGy please add your edit as an additional answer rather than editing your code into someone else's answer. – JAL Jun 22 '16 at 13:49
  • 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. – Erik Kerber Jul 24 '16 at 15:56
  • @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... – Ky - Aug 10 '16 at 21:11
  • 1
    @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. – SimplGy Aug 11 '16 at 17:07
  • only difference b/w Swift 2 & 3 is : `OBJC_ASSOCIATION_RETAIN` ? – Bista Dec 08 '16 at 04:26
  • It works but it creates HUGE LEAKS, especially if you use your properties in a TableViewCell ! – Aximem May 11 '17 at 16:23
  • I just did some testing with Instruments and there was no memory leak unlike @Aximem said. It might have been caused by your UITableView implementation at that time. – Pepijn May 24 '18 at 09:12
  • 25
    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. – iur Jan 21 '19 at 16:33
  • 1
    @jou 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? – Vyachaslav Gerchicov Feb 13 '19 at 14:50
  • 31
    The Swift 4 example doesn't work with multiple instances, and probably should not be here. – Colin Cornaby Mar 11 '19 at 18:22
  • Doesn't work with multiple instances – mrpaw69 Jun 21 '23 at 09:34
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 }
    }
}
Wojciech Nagrodzki
  • 2,788
  • 15
  • 13
  • Ok what to do if I want to store Int, Bool and etc? – Vyachaslav Gerchicov Jun 30 '17 at 13:43
  • 1
    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). – Wojciech Nagrodzki Jul 02 '17 at 21:20
  • 1
    **WARNING** This does not work for structs, because the objective-c runtime library only supports classes that conform to `NSObjectProtocol` – Charlton Provatas Feb 06 '18 at 19:44
  • 2
    @CharltonProvatas It is not possible to use structs with this API, they do not conform to AnyObject protocol. – Wojciech Nagrodzki Feb 09 '18 at 16:34
  • @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. – Wojciech Nagrodzki Feb 14 '19 at 09:17
  • @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 `. – Wojciech Nagrodzki Feb 15 '19 at 09:35
  • 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 remains – Vyachaslav Gerchicov Feb 15 '19 at 10:13
  • @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. – Wojciech Nagrodzki Feb 15 '19 at 12:23
  • 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? – Joaquin Pereira Jun 08 '22 at 18:57
  • @JoaquinPereira Try keeping objects in `NSArray` via `ObjectAssociation` – Wojciech Nagrodzki Jun 09 '22 at 19:22
  • 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 struct – mrpaw69 Jun 21 '23 at 09:44
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)
    }
}
Hannes
  • 3,752
  • 2
  • 37
  • 47
AlexK
  • 638
  • 6
  • 12
  • @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 comment – user2363025 Oct 23 '15 at 07:16
  • 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) ) } } } – user2363025 Oct 23 '15 at 07:16
  • 1
    doesn't work : static var means that the value of the property is the same for all instance – Fry Mar 08 '17 at 15:40
  • @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. – AlexK Jul 23 '17 at 22:37
  • 1
    This is so sexy! Thanks so much! – PhillipJacobs Apr 27 '21 at 19:42
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)
        }
    }
}
picciano
  • 22,341
  • 9
  • 69
  • 82
HepaKKes
  • 1,555
  • 13
  • 22
  • And how do I use the Lifted class? – 3lvis Sep 23 '15 at 05:56
  • Why do you need that? What for? – HepaKKes Sep 29 '15 at 16:40
  • I meant how do you use this in general, thanks for your help :) Can you add a "How to use" example, please? – 3lvis Sep 29 '15 at 18:59
  • 1
    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. – HepaKKes Sep 30 '15 at 11:23
  • 2
    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. – picciano Feb 06 '16 at 23:13
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.

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
Adam Wright
  • 48,938
  • 12
  • 131
  • 152
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.

Amr Hossam
  • 2,313
  • 1
  • 22
  • 23
  • Looks interesting, but refuses doesn't work with protocols, `type 'AssociatedKeys' cannot be defined within a protocol extension`… – Ian Bytchek Jan 25 '16 at 10:00
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
        }
    }
}
valvoline
  • 7,737
  • 3
  • 47
  • 52
  • 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. – Charlton Provatas Jun 07 '18 at 13:41
  • 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). – Arjuna Nov 28 '18 at 14:44
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
    }
}
Alex Marchant
  • 2,490
  • 27
  • 49
vedrano
  • 2,961
  • 1
  • 28
  • 25
  • 4
    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. – picciano Feb 06 '16 at 22:03
  • It war not good because messing up with functions. Now it is better and works as intended. – vedrano Feb 22 '16 at 22:04
  • @picciano `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]`). – vedrano Feb 22 '16 at 22:07
  • @picciano I guess this works for `class` properties ;) – Nicolas Miari Feb 29 '16 at 10:01
  • `frame` is an instance property. Where does class property come from? – vedrano Feb 29 '16 at 12:39
  • The idea is good, much better then using objective-c associated objects. This is the best solution, but suggested implementation is not production ready. – MANIAK_dobrii Aug 18 '16 at 22:18
  • @vedrano do you have any idea how to automatically clear extraData so it doesn't cause memory leaks? – DoubleK Sep 30 '16 at 15:52
  • @DoubleK I would say that you can add the following code: deinit { willDeinit() } so app will make cleanup as soon as UILabel gets released. – vedrano Oct 01 '16 at 19:04
  • 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. – vedrano Oct 01 '16 at 19:33
  • @vedrano , I called willDeinit() inside viewWillDisapper instead of deinit and it works fine – DoubleK Oct 02 '16 at 22:05
  • @DoubleK, yes, that is a nice trick. You loose some generality but it should work just fine for UIView subclass in UIViewController. – vedrano Oct 06 '16 at 08:08
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.

Mike Pollard
  • 10,195
  • 2
  • 37
  • 46
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.

Itachi
  • 5,777
  • 2
  • 37
  • 69
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.

sKhan
  • 9,694
  • 16
  • 55
  • 53
RuiKQ
  • 51
  • 4
  • 3
    This does not really answer the question. If you have a different question, you can ask it by clicking [Ask Question](http://stackoverflow.com/questions/ask). You can also [add a bounty](http://stackoverflow.com/help/privileges/set-bounties) to draw more attention to this question once you have enough [reputation](http://stackoverflow.com/help/whats-reputation). - [From Review](/review/low-quality-posts/11269014) – AtheistP3ace Feb 15 '16 at 13:28
  • @AtheistP3ace 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. – RuiKQ Feb 16 '16 at 01:40
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)
Dan
  • 2,599
  • 1
  • 17
  • 13
  • do you have any idea how to automatically clear extraData so it doesn't cause memory leaks? – DoubleK Sep 30 '16 at 15:52
  • 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. – Dan Oct 02 '16 at 21:59
  • 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 solution – DoubleK Oct 02 '16 at 22:09
  • **DO NOT** try to override parent methods in `extension`. – Itachi Mar 18 '21 at 05:52
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.
Vadim Bulavin
  • 3,697
  • 25
  • 19
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 }
    }

}
Ashkan Ghodrat
  • 3,162
  • 2
  • 32
  • 36
  • 1
    why this answer has no upvotes. Smart solution after all. – byJeevan May 10 '20 at 17:23
  • 1
    This works but I think this approach is flawed as stated here https://medium.com/@valv0/computed-properties-and-extensions-a-pure-swift-approach-64733768112c – Teffi Jul 01 '20 at 16:19
  • Downvote! After `_padding` retains the incoming view object in its key, when will the view be released? – Itachi Mar 18 '21 at 05:49
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
                )
            }
        }
    }

}
abdullahselek
  • 7,893
  • 3
  • 50
  • 40
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")
Kelvin Fok
  • 621
  • 7
  • 9
  • 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. – pronebird Apr 02 '23 at 10:34
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
        }
    }
}
ChanOnly123
  • 1,004
  • 10
  • 12
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)
    }
}
Vadims Krutovs
  • 197
  • 2
  • 3
  • 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. – kelin Dec 02 '16 at 10:35
  • Yes you right, this not working for all the instances. I Fixed by storing min and max value in struct. Sorry for off topic. – Vadims Krutovs Dec 03 '16 at 08:47
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
}
droid
  • 581
  • 7
  • 16
-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 )
  • 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. – Nick Hingston Aug 09 '22 at 17:20
-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
Vassily
  • 899
  • 2
  • 8
  • 19
-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

    }
}