Je permute une application d'Objective-C à Swift, pour laquelle j'ai plusieurs catégories avec des propriétés stockées, par exemple:
@interface UIView (MyCategory)
- (void)alignToView:(UIView *)view
alignment:(UIViewRelativeAlignment)alignment;
- (UIView *)clone;
@property (strong) PFObject *xo;
@property (nonatomic) BOOL isAnimating;
@end
Comme les extensions Swift n'acceptent pas les propriétés stockées comme celles-ci, je ne sais pas comment conserver la même structure que le code Objc. Les propriétés stockées sont très importantes pour mon application et je pense qu'Apple doit avoir créé une solution pour le faire dans Swift.
Comme dit par Jou, ce que je cherchais, c’était en fait d’utiliser des objets associés, alors j’ai (dans un autre contexte):
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))
}
}
}
Mais je reçois un EXC_BAD_ACCESS lorsque je fais:
class UIBubble : UIView {
required init(coder aDecoder: NSCoder) {
...
self.layer.shapeLayer = CAShapeLayer()
...
}
}
Des idées?
L'API d'objets associés est un peu fastidieux à utiliser. Vous pouvez supprimer la majeure partie du passe-partout avec une classe d'assistance.
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) }
}
}
Pourvu que vous puissiez "ajouter" une propriété à la classe objective-c d'une manière plus lisible:
extension SomeType {
private static let association = ObjectAssociation<NSObject>()
var simulatedProperty: NSObject? {
get { return SomeType.association[self] }
set { SomeType.association[self] = newValue }
}
}
En ce qui concerne la 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 }
}
}
Comme dans Objective-C, vous ne pouvez pas ajouter de propriété stockée à des classes existantes. Si vous étendez une classe Objective-C (UIView
en est une), vous pouvez toujours utiliser Objets associés pour émuler les propriétés stockées:
pour 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))
}
}
}
La clé d'association est un pointeur qui devrait être unique pour chaque association. Pour cela, nous créons une variable globale privée et utilisons son adresse mémoire comme clé avec l'opérateur &
. Voir le Utilisation de Swift avec Cocoa et Objective-C sur plus de détails sur la gestion des pointeurs dans Swift.
UPDATED pour Swift 2 et 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)
}
}
}
MISE À JOUR pour Swift 4
Dans Swift 4, c'est beaucoup plus simple. La structure Holder contiendra la valeur privée que notre propriété calculée exposera au monde, donnant l'illusion d'un comportement de propriété stockée.
extension UIViewController {
struct Holder {
static var _myComputedProperty:Bool = false
}
var myComputedProperty:Bool {
get {
return Holder._myComputedProperty
}
set(newValue) {
Holder._myComputedProperty = newValue
}
}
}
Je pense donc avoir trouvé une méthode plus efficace que les précédentes, car elle ne nécessite aucune variable globale. Je l'ai eu d'ici: http://nshipster.com/Swift-objc-runtime/
Le Gist est que vous utilisez une structure comme ceci:
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 pour 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)
}
}
La solution indiquée par jou ne supporte pas les types de valeur , Cela fonctionne très bien aussi
Enveloppes
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
}
}
Un possible Extension de classe (Exemple d'utilisation):
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)
}
}
}
}
C'est vraiment une si bonne solution, je voulais ajouter un autre exemple d'utilisation incluant des structures et des valeurs qui ne sont pas des options. En outre, les valeurs AssociatedKey peuvent être simplifiées.
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)
}
}
}
Vous ne pouvez pas définir de catégories (extensions Swift) avec un nouveau stockage. toute propriété supplémentaire doit être calculée plutôt que stockée. La syntaxe fonctionne pour Objective C car @property
dans une catégorie signifie essentiellement "je fournirai le getter et le setter". Dans Swift, vous devrez les définir vous-même pour obtenir une propriété calculée. quelque chose comme:
extension String {
public var Foo : String {
get
{
return "Foo"
}
set
{
// What do you want to do here?
}
}
}
Devrait bien fonctionner. N'oubliez pas que vous ne pouvez pas stocker de nouvelles valeurs dans le setter, vous ne pouvez travailler qu'avec l'état de classe disponible existant.
Mon 0,02 $. Ce code est écrit en 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)
}
}
}
}
J'ai essayé de nombreuses solutions et j'ai trouvé que c'était le seul moyen d'étendre une classe avec des paramètres variables supplémentaires.
Pourquoi compter sur objc runtime? Je ne comprends pas le point. En utilisant quelque chose comme ce qui suit, vous obtiendrez presque le comportement identique d’une propriété stockée, en utilisant seulement une approche pure 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
}
}
}
Je préfère utiliser du code purement Swift et ne pas compter sur l'héritage Objective-C. A cause de cela, j'ai écrit une solution Swift pure avec deux avantages et deux inconvénients.
Avantages:
Code Swift pur
Fonctionne sur les classes et les complétions ou plus spécifiquement sur l'objet Any
Désavantages:
Le code doit appeler la méthode willDeinit()
pour libérer les objets liés à une instance de classe spécifique afin d'éviter les fuites de mémoire.
Vous ne pouvez pas créer d’extension directement vers UIView pour cet exemple exact, car var frame
est une extension de UIView et ne fait pas partie de la classe.
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
}
}
Avec Obj-c Categories, vous ne pouvez ajouter que des méthodes, pas des variables d'instance.
Dans votre exemple, vous avez utilisé @property comme raccourci pour ajouter des déclarations de méthodes getter et setter. Vous devez toujours implémenter ces méthodes.
De même, dans Swift, vous pouvez ajouter des extensions use pour ajouter des méthodes d'instance, des propriétés calculées, etc.
Voici une solution simplifiée et plus expressive. Cela fonctionne à la fois pour les types valeur et référence. L’approche de levage est tirée de la réponse de @HepaKKes.
Code d'association:
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)
}
}
Exemple d'utilisation:
1) Créez une extension et associez-lui des propriétés. Utilisons les propriétés de type valeur et référence.
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) Vous pouvez maintenant utiliser les propriétés habituelles:
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
Plus de détails:
J'ai essayé d'utiliser objc_setAssociatedObject comme mentionné dans quelques réponses, mais après avoir échoué plusieurs fois, je me suis retiré et j'ai réalisé que je n'avais aucune raison de le faire. En empruntant quelques idées ici, j'ai créé ce code qui stocke simplement un tableau de toutes mes données supplémentaires (MyClass dans cet exemple) indexées par l'objet auquel je veux l'associer:
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)
J'ai aussi un problème EXC_BAD_ACCESS.La valeur dans objc_getAssociatedObject()
et objc_setAssociatedObject()
doit être un objet Et le objc_AssociationPolicy
devrait correspondre à l'objet.
Un autre exemple d'utilisation d'objet associé à Objective-C et de propriétés calculées pour Swift 3 et 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
)
}
}
}
}
si vous souhaitez définir un attribut de chaîne personnalisé pour un UIView, voici comment je l'ai fait sur Swift 4
Créer une extension 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
}
}
Pour utiliser cette extension
let key = "COLOR"
let redView = UIView()
// To set
redView.setStringAttribute(value: "Red", key: key)
// To read
print(redView.stringValueFor(key: key)) // Optional("Red")
Que diriez-vous de stocker une carte statique dans une classe qui s'étend comme ceci:
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 }
}
}
J'ai essayé de stocker des propriétés en utilisant objc_getAssociatedObject, objc_setAssociatedObject, sans aucune chance. Mon objectif était de créer une extension pour UITextField, afin de valider la longueur des caractères de saisie de texte. Le code suivant fonctionne bien pour moi. J'espère que cela aidera quelqu'un.
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)
}
}
Voici une alternative qui fonctionne aussi
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 )