Skip to content

Instantly share code, notes, and snippets.

@hooman
Last active May 4, 2022 07:33
Show Gist options
  • Save hooman/599e381d5f037b87d20b to your computer and use it in GitHub Desktop.
Save hooman/599e381d5f037b87d20b to your computer and use it in GitHub Desktop.
Take 2.0: A completely new take on Objective-C associated types. (See history for the ancient approach)
// Playground - noun: a place where people can play
//
//
// By: Hooman Mehr (hooman@mac.com)
// Gist: https://gist.github.com/hooman/599e381d5f037b87d20b (The latest version is always here)
//
//
// Update: 07/30/2014
// Added WeaklyOwned & removeOrphans for memory management support, added more generics & basic access control.
// 08/06/2014
// Swift Beta 5 compatibility
// 09/10/2014
// Some cleanup, verified to work with Xcode 6 GM & 6.1 beta 1.
// 05/03/2022
//. Rewritten with a new approach
//
//
// WARNING: This code is for illustrative purposes, it is *not* thread safe and memory overhead and access
// performance of properties added using associated objects may not be acceptable in practice.
//
import Foundation
/// A protocol to facilitate creating Objective-C associated objects.
///
/// This protocol along with its standard implementation makes it easier to attach an object (an
/// associated object) to any Objective-C `NSObject` subclass.
///
/// The implementation of the protocol is provided. You just need to provide a default initializer
/// and declare conformance. This protocol require a default (no-arg) `init` to avoid object
/// initialization complications.
///
public protocol AssociatedObject: AnyObject {
/// The key used to associate the objects of this type.
static var key: UnsafeRawPointer { get }
/// Creates a new associated object with properties initialized to default values.
///
/// An associated object is rarely initialized explicitly. Usually a helper function creates and
/// associates the object in one step.
init()
}
// The implementation of `AssociatedObject` protocol
public extension AssociatedObject {
static var key: UnsafeRawPointer { UnsafeRawPointer(bitPattern: UInt(bitPattern: ObjectIdentifier(Self.self)))! }
}
// An extension to `NSObject` to manage associated objects.
public extension NSObject {
/// Returns the associated object of the given type to this object.
///
/// If an object of the given type was not already associated,
/// an object is created, associated and returned.
/// - Parameter : The type of the associated object.
/// - Returns: The associated object.
///
func associated<Object: AssociatedObject>(_ objectClass: Object.Type ) -> Object {
var object = objc_getAssociatedObject(self, Object.key) as? Object
if object == nil {
object = objectClass.init()
objc_setAssociatedObject(self, Object.key, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return object!
}
/// Disassociates the associated object of the given type from this object.
///
/// It also returns the associated object if it exists.
/// - Parameter : The type of the associated object.
/// - Returns: The previously associated object if it exists. Otherwise returns `nil`
///
@discardableResult
func disassociate<Object: AssociatedObject>(_ objectClass: Object.Type ) -> Object? {
let object = objc_getAssociatedObject(self, Object.key) as? Object
if let _ = object {
objc_setAssociatedObject(self, Object.key, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return object
}
}
// =============================
// How to Use:
// =============================
/// If you only need to associate a single existing object, then you
/// need to declare the conformity of its class to `AssociatedObject` and simply use
/// provided `associated(_:)` method on any object to associate and access the associated object.
///
// Example existing class to become an associated object:
final class ExtraObject {
var anInt: Int = 0
var aString: String = ""
}
// Declare conformance:
extension ExtraObject: AssociatedObject {}
// Here is another object to be the target of association:
class MyObject: NSObject {}
let myObject = MyObject()
// Associate an `ExtraObject` with `myObject` and do something with it:
myObject.associated(ExtraObject.self).aString = "Just like an added property"
print(myObject.associated(ExtraObject.self).aString)
// You can also add properties to a class with an extension:
extension MyObject {
var anInt: Int {
get { associated(ExtraObject.self).anInt }
set { associated(ExtraObject.self).anInt = newValue }
}
var aString: String {
get { associated(ExtraObject.self).aString }
set { associated(ExtraObject.self).aString = newValue }
}
}
// You can also use it to conform a class to a stateful protocol:
protocol StatefulProtocol {
var someState: String { get set }
var anotherState: Int { get set }
}
extension MyObject: StatefulProtocol {
final class State: AssociatedObject {
var someState: String = ""
var anotherState: Int = 0
}
var someState: String {
get { associated(State.self).someState }
set { associated(State.self).someState = newValue }
}
var anotherState: Int {
get { associated(State.self).anotherState }
set { associated(State.self).anotherState = newValue }
}
}
let objects = [ MyObject(), MyObject(), MyObject() ]
for i in objects.indices { objects[i].anInt = i+1; objects[i].anotherState = 10 * i + 5 }
for o in objects { print(o.anInt, o.anotherState) }
for o in objects { o.disassociate(ExtraObject.self) }
for o in objects { print(o.anInt, o.anotherState) }
for o in objects { o.disassociate(MyObject.State.self) }
for o in objects { print(o.anInt, o.anotherState) }
@mattneub
Copy link

Broken by seed 4, which has done away with CString.

@hooman
Copy link
Author

hooman commented Jul 22, 2014

Fixed for b4 now. Also pure Swift now works in playground.

@hooman
Copy link
Author

hooman commented Jul 30, 2014

Update: Provided a solution for avoiding memory leak in pure Swift version and did some cleanup.

@hooman
Copy link
Author

hooman commented Aug 6, 2014

Update: Swift Beta 5 Compatibility

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment