Как сохранить свойства в 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()
...
}
}
Есть идеи?
22 ответа
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 }
}
}
Как и в 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 с Какао и 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
}
}
}
Поэтому я думаю, что нашел метод, который работает чище, чем приведенный выше, потому что он не требует глобальных переменных. Я получил это отсюда: 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)
}
}
Решение, на которое указывает 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)
}
}
}
Вы не можете определять категории (расширения Swift) с новым хранилищем; любые дополнительные свойства должны быть вычислены, а не сохранены. Синтаксис работает для Objective C, потому что @property
в категории, по сути, означает "я предоставлю геттер и сеттер". В Swift вам нужно определить их самостоятельно, чтобы получить вычисляемое свойство; что-то вроде:
extension String {
public var Foo : String {
get
{
return "Foo"
}
set
{
// What do you want to do here?
}
}
}
Должно работать нормально. Помните, что вы не можете хранить новые значения в установщике, работайте только с существующим доступным состоянием класса.
Мои $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)
}
}
}
}
Я перепробовал много решений и обнаружил, что это единственный способ расширить класс с помощью дополнительных переменных параметров.
Зачем полагаться на время выполнения 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
}
}
}
Я предпочитаю делать код на чистом Swift и не полагаться на наследие Objective-C. Из-за этого я написал чистое решение Swift с двумя преимуществами и двумя недостатками.
Преимущества:
Чистый код Swift
Работает на классах и дополнениях или, более конкретно, на
Any
объект
Недостатки:
Код должен вызывать метод
willDeinit()
освободить объекты, связанные с конкретным экземпляром класса, чтобы избежать утечек памятиВы не можете сделать расширение непосредственно для UIView для этого точного примера, потому что
var frame
является расширением UIView, не является частью класса.
РЕДАКТИРОВАТЬ:
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
}
}
С категориями Obj-c вы можете добавлять только методы, а не переменные экземпляра.
В вашем примере вы использовали @property как ярлык для добавления объявлений методов getter и setter. Вам все еще нужно реализовать эти методы.
Аналогично в Swift вы можете добавить использование расширений для добавления методов экземпляров, вычисляемых свойств и т. Д., Но не сохраненных свойств.
Во- первых, Associated Objects должны быть лучшим правильным решением для расширенных хранимых свойств, потому что они происходят из среды выполнения Objective-C, это отличная мощная функция, которую мы должны использовать до того, как появятся другие нативные функции языка Swift.
Вы всегда должны знать, что связанные объекты будут освобождены после того, как не останется других объектов для их сохранения, включая быстрые объекты, поэтому не используйте настраиваемые контейнеры для сохранения целевых значений, которые не будут освобождены автоматически.
Во-вторых, для этих дополнительных связанных определений ключевой структуры базовые функции просто нуждаются в
UnsafeRawPointer
для этого на самом деле есть еще один лучший выбор,
#function
является статической строкой, сгенерированной при компиляции исходного кода, она также имеет собственный адрес для использования.
Итак, вот оно:
var status: Bool? {
get { objc_getAssociatedObject(self, #function) as? Bool }
set { objc_setAssociatedObject(self, #function, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)}
}
Сборка для свифт 5.
Наконец, имейте в виду тип объекта с политикой ассоциации.
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 }
}
}
Я попытался использовать 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)
Вот упрощенное и более выразительное решение. Он работает как для значений, так и для ссылочных типов. Подход подъема взят из ответа @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
Больше деталей:
- Подъем необходим для упаковки значений типов.
- Поведение связанного объекта по умолчанию сохраняется. Если вы хотите узнать больше о связанных объектах, я бы порекомендовал проверить эту статью.
Я также получаю проблему EXC_BAD_ACCESS. Значение в objc_getAssociatedObject()
а также objc_setAssociatedObject()
должен быть объектом. И objc_AssociationPolicy
должен соответствовать объекту.
В PURE SWIFT со СЛАБОЙ обработкой ссылок
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
}
}
}
Если вы хотите установить пользовательский атрибут строки в 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")
Другой пример использования связанных объектов 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
)
}
}
}
}
Почему бы просто не сделать что-то подобное, я вижу, что другие решения не нужны.
private var optionalID: String {
UUID().uuidString
}
Я попытался сохранить свойства, используя 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)
}
}
этот пример кода работает для меня и исправляет утечку памяти.
СВИФТ 5.2 (iOS 14+)
(проверено на игровой площадке, для проверки можно скопировать/вставить)
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
Вот альтернатива, которая работает также
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 )
Я нашел это решение более практичным
ОБНОВЛЕНО для 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
}
}