wake-up-neo.com

Wie lassen sich Eigenschaften in Swift speichern, genau wie bei Objective-C?

Ich wechsle eine Anwendung von Objective-C zu Swift, für die ich einige Kategorien mit gespeicherten Eigenschaften habe, zum Beispiel:

@interface UIView (MyCategory)

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

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

@end

Da Swift-Erweiterungen solche Eigenschaften nicht akzeptieren, weiß ich nicht, wie ich die gleiche Struktur wie den Objc-Code beibehalten soll. Gespeicherte Eigenschaften sind für meine App sehr wichtig, und ich glaube, Apple muss eine Lösung dafür geschaffen haben, um dies in Swift zu tun.

Wie von Jou gesagt, was ich suchte, waren eigentlich verwandte Objekte, also tat ich es (in einem anderen Kontext):

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

Ich erhalte jedoch einen EXC_BAD_ACCESS, wenn ich Folgendes tue:

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

Irgendwelche Ideen?

124
Marcos Duarte

Zugehörige Objekte Die API ist etwas umständlich zu verwenden. Sie können den Großteil der Heizplatte mit einer Hilfsklasse entfernen.

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

Vorausgesetzt, Sie können der object-c-Klasse eine Eigenschaft lesbarer "hinzufügen":

extension SomeType {

    private static let association = ObjectAssociation<NSObject>()

    var simulatedProperty: NSObject? {

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

Wie für die Lösung:

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

Wie in Objective-C können Sie keine gespeicherten Eigenschaften zu vorhandenen Klassen hinzufügen. Wenn Sie eine Objective-C-Klasse erweitern (UIView ist definitiv eine), können Sie weiterhin Associated Objects verwenden, um gespeicherte Eigenschaften zu emulieren:

für 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))
        }
    }
}

Der Zuordnungsschlüssel ist ein Zeiger, der für jede Zuordnung eindeutig sein sollte. Dazu erstellen wir eine private globale Variable und verwenden deren Speicheradresse als Schlüssel mit dem Operator &. Siehe Swift mit Cocoa und Objective-C verwenden on mehr Details, wie Zeiger in Swift behandelt werden.

AKTUALISIERT für Swift 2 und 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)
        }
    }
}

AKTUALISIERT für Swift 4

In Swift 4 ist es viel einfacher. Die Holder-Struktur wird den privaten Wert enthalten, den unser berechnetes Eigentum der Welt offenbaren wird, wodurch die Illusion eines gespeicherten Eigenschaftsverhaltens entsteht.

Quelle

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

Ich glaube, ich habe eine Methode gefunden, die sauberer ist als die oben genannten, da keine globalen Variablen erforderlich sind. Ich habe es von hier bekommen: http://nshipster.com/Swift-objc-runtime/

Der Kern ist, dass Sie eine Struktur wie folgt verwenden:

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 für 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)
    }
}
35
AlexK

Die von jou aufgezeigte Lösung unterstützt value types , Dies funktioniert auch bei ihnen

Wrapper

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

Eine mögliche Klassenerweiterung (Verwendungsbeispiel):

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

Dies ist wirklich eine großartige Lösung. Ich wollte ein weiteres Verwendungsbeispiel hinzufügen, das Strukturen und Werte enthält, die keine optionalen Elemente sind. Die AssociatedKey-Werte können auch vereinfacht werden.

struct Crate {
    var name: String
}

class Box {
    var name: String

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

extension UIViewController {

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

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

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

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

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

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

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

Sie können keine Kategorien (Swift-Erweiterungen) mit neuem Speicher definieren. zusätzliche Eigenschaften müssen berechnet nicht gespeichert werden. Die Syntax funktioniert für Objective C, da @property in einer Kategorie im Wesentlichen bedeutet "Ich gebe den Getter und den Setter". In Swift müssen Sie diese selbst definieren, um eine berechnete Eigenschaft zu erhalten. so etwas wie:

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

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

Sollte gut funktionieren. Denken Sie daran, dass Sie keine neuen Werte im Setter speichern können, sondern nur mit dem vorhandenen verfügbaren Klassenstatus arbeiten.

12
Adam Wright

Meine $ 0,02. Dieser Code ist in Swift 2.0 geschrieben.

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

Ich habe viele Lösungen ausprobiert und festgestellt, dass dies die einzige Möglichkeit ist, eine Klasse um zusätzliche variable Parameter zu erweitern.

7
Amro Shafie

Warum sollte man sich auf die objc-Laufzeit verlassen? Ich verstehe nicht den Punkt. Mit etwas wie dem folgenden erreichen Sie fast dasselbe Verhalten einer gespeicherten Eigenschaft, indem Sie nur einen pure Swift-Ansatz verwenden:

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

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

Ich bevorzuge Code in Pure Swift und verlasse mich nicht auf Objective-C. Aus diesem Grund habe ich eine reine Swift-Lösung mit zwei Vorteilen und zwei Nachteilen geschrieben.

Vorteile:

  1. Pure Swift-Code

  2. Funktioniert für Klassen und Vervollständigungen oder genauer für Any-Objekt

Nachteile:

  1. Code sollte die Methode willDeinit() aufrufen, um Objekte freizugeben, die mit einer bestimmten Klasseninstanz verknüpft sind, um Speicherverluste zu vermeiden

  2. Sie können die Erweiterung für dieses genaue Beispiel nicht direkt an UIView vornehmen, da var frame eine Erweiterung für UIView ist und nicht Teil der Klasse ist.

BEARBEITEN:

import UIKit

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

var didSetFrame_ = "didSetFrame"

extension UILabel {

    override public var frame: CGRect {

        get {
            return didSetFrame ?? CGRectNull
        }

        set {
            didSetFrame = newValue
        }
    }

    var didSetFrame: CGRect? {

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

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

            selfDictionary[didSetFrame_] = newValue

            extensionPropertyStorage[self] = selfDictionary
        }
    }

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

Mit Obj-c Categories können Sie nur Methoden hinzufügen, keine Instanzvariablen.

In Ihrem Beispiel haben Sie @property als Abkürzung für das Hinzufügen von Getter- und Setter-Methodendeklarationen verwendet. Sie müssen diese Methoden noch implementieren.

Auf ähnliche Weise können Sie in Swift Verwendungserweiterungen hinzufügen, um Instanzmethoden, berechnete Eigenschaften usw. hinzuzufügen, jedoch keine gespeicherten Eigenschaften.

3
Mike Pollard

Hier ist eine vereinfachte und ausdrucksstärkere Lösung. Es funktioniert sowohl für Wert- als auch für Referenztypen. Der Ansatz des Anhebens ist der Antwort von @HepaKKes entnommen.

Assoziationscode:

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

Verwendungsbeispiel:

1) Erstellen Sie eine Erweiterung und ordnen Sie ihr Eigenschaften zu. Wir verwenden sowohl Werte als auch Referenztypeigenschaften.

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) Jetzt können Sie nur noch normale Eigenschaften verwenden:

    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

Mehr Details:

  1. Beim Umwickeln von Werttypen ist ein Abheben erforderlich. 
  2. Das standardmäßige Verhalten für zugeordnete Objekte bleibt erhalten. Wenn Sie mehr über zugehörige Objekte erfahren möchten, würde ich empfehlen, diesen Artikel zu überprüfen.
2
Vadim Bulavin

Ich habe versucht, objc_setAssociatedObject zu verwenden, wie in einigen Antworten erwähnt, aber nachdem ich einige Male versagt hatte, trat ich zurück und erkannte, dass es keinen Grund gibt, warum ich das brauche. Ich habe mir ein paar Ideen dazu geborgt und diesen Code entwickelt, der einfach ein Array von meinen zusätzlichen Daten speichert (in diesem Beispiel MyClass), die durch das Objekt indiziert werden, mit dem ich es verknüpfen möchte:

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

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

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

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

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

Ich erhalte auch ein EXC_BAD_ACCESS-Problem. Der Wert in objc_getAssociatedObject() und objc_setAssociatedObject() sollte ein Objekt sein. Und der objc_AssociationPolicy sollte mit dem Objekt übereinstimmen.

2
RuiKQ

Ein weiteres Beispiel mit der Verwendung von Objective-C-Objekten und berechneten Eigenschaften für Swift 3 und Swift 4

import CoreLocation

extension CLLocation {

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

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

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

}
1
abdullahselek

wenn Sie ein benutzerdefiniertes Zeichenfolgenattribut für ein UIView festlegen möchten, habe ich es in Swift 4 so gemacht

Erstellen Sie eine UIView-Erweiterung

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

So verwenden Sie diese Erweiterung

let key = "COLOR"

let redView = UIView() 

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

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

Wie wäre es mit dem Speichern einer statischen Karte in einer Klasse, die sich wie folgt erweitert:

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

}
0
Ashkan Ghodrat

Ich habe versucht, Eigenschaften zu speichern, indem ich objc_getAssociatedObject, objc_setAssociatedObject, ohne Erfolg verwendet habe. Mein Ziel war es, eine Erweiterung für UITextField zu erstellen, um die Länge der Texteingabezeichen zu überprüfen. Der folgende Code funktioniert gut für mich. Hoffe das hilft jemandem. 

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

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

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

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

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

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

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

        return (valid, error)
    }
}
0
Vadims Krutovs

Hier ist eine Alternative, die auch funktioniert

public final class Storage : AnyObject {

    var object:Any?

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

extension Date {

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

    public var locale:Locale? {
        get {

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



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