Как сохранить свойства в Swift, так же, как у меня на Objective-C?

Я переключаю приложение из Objective-C в Swift, у которого есть несколько категорий с сохраненными свойствами, например:

@interface UIView (MyCategory)

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

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

@end

Поскольку расширения Swift не принимают хранимые свойства, подобные этим, я не знаю, как поддерживать ту же структуру, что и код Objc. Сохраненные свойства действительно важны для моего приложения, и я считаю, что Apple, должно быть, создала какое-то решение для этого в Swift.

Как сказал Джоу, то, что я искал, фактически использовало связанные объекты, поэтому я сделал (в другом контексте):

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

Но я получаю EXC_BAD_ACCESS, когда делаю:

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

Любые идеи?

Ответ 1

API связанных объектов немного неудобен в использовании. Вы можете удалить большую часть шаблона с помощью вспомогательного класса.

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

При условии, что вы можете "добавить" свойство в class-c более читабельным образом:

extension SomeType {

    private static let association = ObjectAssociation<NSObject>()

    var simulatedProperty: NSObject? {

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

Что касается решения:

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

Ответ 2

Как и в Objective-C, вы не можете добавить сохраненное свойство к существующим классам. Если вы расширяете класс Objective-C (UIView определенно один), вы все равно можете использовать связанные объекты для эмуляции хранимых свойств:

для 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))
        }
    }
}

Ключ ассоциации - это указатель, который должен быть уникальным для каждой ассоциации. Для этого мы создаем приватную глобальную переменную и используем ее адрес памяти в качестве ключа с оператором &. См. Использование Swift с Cocoa и Objective-C для более подробной информации о том, как обрабатываются указатели в Swift.

ОБНОВЛЕНО для Swift 2 и 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)
        }
    }
}

ОБНОВЛЕНО для Swift 4

В Swift 4 все гораздо проще. Структура Holder будет содержать частную ценность, которую наше вычисленное свойство представит миру, создавая иллюзию поведения хранимых свойств.

Источник

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

Ответ 3

Поэтому я думаю, что нашел метод, который работает более чистым, чем те, что были выше, потому что он не требует глобальных переменных. Я получил его отсюда: http://nshipster.com/swift-objc-runtime/

Суть в том, что вы используете такую ​​структуру:

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

ОБНОВЛЕНИЕ для 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)
    }
}

Ответ 4

Решение, указанное jou, не поддерживает типы значений, это прекрасно работает с ними.

Упаковщики

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

Возможно Расширение класса (пример использования):

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

Это действительно отличное решение, я хотел добавить еще один пример использования, включающий в себя структуры и значения, которые не являются необязательными. Кроме того, значения AssociatedKey могут быть упрощены.

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

Ответ 5

Вы не можете определить категории (Swift extensions) с новым хранилищем; любые дополнительные свойства должны вычисляться, а не сохраняться. Синтаксис работает для Objective C, потому что @property в категории по существу означает "я предоставил getter и setter". В Swift вам необходимо определить их самостоятельно, чтобы получить вычисленное свойство; что-то вроде:

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

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

Должно работать нормально. Помните, что вы не можете хранить новые значения в сеттере, работайте только с существующим доступным состоянием класса.

Ответ 6

Мои $0,02. Этот код написан в 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)
            }
        }
    }
}

Я пробовал много решений и нашел, что это единственный способ фактически расширить класс с дополнительными параметрами переменной.

Ответ 7

Я предпочитаю делать код в чистом Swift и не полагаться на наследие Objective-C. Из-за этого я написал чистое решение Swift с двумя преимуществами и двумя недостатками.

<сильные > Преимущества:

  • Код Pure Swift

  • Работает с классами и дополнениями или, более конкретно, на Any object

Недостатки:

  • Код должен вызывать метод willDeinit() для выпуска объектов, связанных с конкретным экземпляром класса, чтобы избежать утечек памяти.

  • Вы не можете сделать расширение непосредственно в UIView для этого точного примера, потому что var frame является расширением для UIView, а не частью класса.

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

Ответ 8

Зачем полагаться на время выполнения objc? Я не понимаю, в чем суть. Используя что-то вроде следующего, вы достигнете почти идентичного поведения хранимого свойства, используя только чистый подход Swift:

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

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

Ответ 9

С категориями Obj-c вы можете добавлять только методы, а не переменные экземпляра.

В примере вы использовали @property как ярлык для добавления объявлений метода getter и setter. Вам все равно необходимо реализовать эти методы.

Аналогично в Swift вы можете добавлять расширения для добавления методов экземпляра, вычисленных свойств и т.д., но не сохраняемых свойств.

Ответ 10

Я также получаю проблему с EXC_BAD_ACCESS. Значение в objc_getAssociatedObject() и objc_setAssociatedObject() должно быть объектом. И objc_AssociationPolicy должен соответствовать объекту.

Ответ 11

Я попытался использовать objc_setAssociatedObject, как упоминалось в нескольких ответах здесь, но после неудачной попытки несколько раз я отступил назад и понял, что мне не нужна эта причина. Заимствуя из нескольких идей здесь, я придумал этот код, который просто хранит массив любых моих дополнительных данных (MyClass в этом примере), проиндексированных объектом, с которым я хочу связать его:

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)

Ответ 12

Вот упрощенное и более выразительное решение. Он работает как для значений, так и для ссылочных типов. Подъем подъема берется из ответа @HepaKKes.

Код ассоциации:

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

Пример использования:

1) Создайте расширение и свяжите с ним свойства. Позвольте использовать свойства значения и ссылочного типа.

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) Теперь вы можете использовать как обычные свойства:

    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

Подробнее:

  • Подъем необходимо для обертывания типов значений.
  • Поведение по умолчанию, связанное с объектом, сохраняется. Если вы хотите узнать больше о связанных объектах, я бы рекомендовал проверить эту статью.

Ответ 13

Другой пример использования Objective-C связанных объектов и вычисленных свойств для Swift 3 и 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
                )
            }
        }
    }

}

Ответ 14

Я попытался сохранить свойства с помощью objc_getAssociatedObject, objc_setAssociatedObject, без всякой удачи. Моя цель - создать расширение для UITextField, чтобы проверить длину символов ввода текста. Следующий код работает отлично для меня. Надеюсь, это поможет кому-то.

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

Ответ 15

Вот альтернатива, которая работает также

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 )

Ответ 16

если вы хотите установить пользовательский атрибут строки в UIView, это то, как я сделал это на Swift 4

Создать расширение UIView

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

Чтобы использовать это расширение

let key = "COLOR"

let redView = UIView() 

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

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

Ответ 17

Как насчет хранения статической карты в классе, который расширяется так:

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

}

Ответ 18

Я нашел это решение более практичным

ОБНОВЛЕНО для 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)
    }

}

... затем в вашем коде

class gameController: UIViewController {

    @IBOutlet var game: gameClass!

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

    }
}