Can a Swift Property Wrapper Reference the Owner of the Property Its Wrapping

Can a Swift Property Wrapper reference the owner of the property its wrapping?

The answer is no, it's not possible with the current specification.

I wanted to do something similar. The best I could come up with was to use reflection in a function at the end of init(...). At least this way you can annotate your types and only add a single function call in init().


fileprivate protocol BindableObjectPropertySettable {
var didSet: () -> Void { get set }
}

@propertyDelegate
class BindableObjectProperty<T>: BindableObjectPropertySettable {
var value: T {
didSet {
self.didSet()
}
}
var didSet: () -> Void = { }
init(initialValue: T) {
self.value = initialValue
}
}

extension BindableObject {
// Call this at the end of init() after calling super
func bindProperties(_ didSet: @escaping () -> Void) {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if var child = child.value as? BindableObjectPropertySettable {
child.didSet = didSet
}
}
}
}

Property wrappers and SwiftUI environment: how can a property wrapper access the environment of its enclosing object?

With Xcode 13 (haven't tested on earlier versions) as long as your property wrapper implements DynamicProperty you can use the @Environment property wrapper.

The following example create a property wrapper that's read the lineSpacing from the current environment.

@propertyWrapper
struct LineSpacing: DynamicProperty {
@Environment(\.lineSpacing) var lineSpacing: CGFloat

var wrappedValue: CGFloat {
lineSpacing
}
}

Then you can use it just like any other property wrapper:

struct LineSpacingDisplayView: View {
@LineSpacing private var lineSpacing: CGFloat

var body: some View {
Text("Line spacing: \(lineSpacing)")
}
}

struct ContentView: View {
var body: some View {
VStack {
LineSpacingDisplayView()
LineSpacingDisplayView()
.environment(\.lineSpacing, 99)
}
}
}

This displays:

Line spacing: 0.000000

Line spacing: 99.000000

How to build a Swift object that can control the mutability of its stored properties


import Foundation

protocol PropertyWrapperWithLockableObject {
var enclosingObject: LockableObjectBase! {get set}
}

@propertyWrapper
class Lockable<Value>: PropertyWrapperWithLockableObject {
private var _wrappedValue: Value
var enclosingObject: LockableObjectBase!

init (wrappedValue: Value) { self._wrappedValue = wrappedValue }

var wrappedValue: Value {
get {
precondition(enclosingObject.isLocked, "Cannot access object properties until object is locked")
return _wrappedValue
}
set {
precondition(!enclosingObject.isLocked, "Cannot modify object properties after object is locked")
_wrappedValue = newValue
}
}
}

class LockableObjectBase {
internal var isLocked: Bool = false {
didSet { isLocked = true }
}

init () {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if var child = child.value as? PropertyWrapperWithLockableObject {
child.enclosingObject = self
}
}
}
}

Usage:

class DataObject: LockableObjectBase {
@Lockable var someString: String = "Zork"
@Lockable var someInt: Int

override init() {
someInt = 42
// super.init() // Not needed in this particular example.
}
}

var testObject = DataObject()
testObject.isLocked = true
print(testObject.someInt, testObject.someString) // 42, Zork
testObject.isLocked = false // Has no effect: Object remained locked
print (testObject.isLocked) // true
testObject.someInt = 2 // Aborts the program

arsenius's answer here provided the vital reflection clue!

Add @Published behaviour for computed property

Updated:
With the EnclosingSelf subscript, one can do it!

Works like a charm!

import Combine
import Foundation

class LocalSettings: ObservableObject {
static var shared = LocalSettings()

@Setting(key: "TabSelection")
var tabSelection: Int = 0
}

@propertyWrapper
struct Setting<T> {
private let key: String
private let defaultValue: T

init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}

var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}

public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
) -> T {
get {
return object[keyPath: storageKeyPath].wrappedValue
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
}
}
}

Swift 5.1 @propertyWrapper - 'self' used in property access before all stored properties are initialized


EDIT:

Actually a better workaround would be to directly use _foo.wrappedValue.uppercased() instead of foo.uppercased().

This solves also the other issue with the double initialization.

Thinking deeper about this, that's definitely the intended behavior.

If I understand it correctly, in OhNoes, foo is just short for:

var foo: String {
get {
return self._foo.wrappedValue
}
set {
self._foo.wrappedValue = newValue
}
}

So there is no way this could work in any other way.


I was facing the same issue you had and I actually think that it is some sort of bug/unwanted behavior.

Anyway the best I could go out with is this:

@propertyWrapper
struct Box<Value> {
private var box: Value

init(wrappedValue: Value) {
box = wrappedValue
}

var wrappedValue: Value {
get { box }
set { box = newValue }
}
}

class OhNoes {
@Box var foo : String
let upperCased: String

init() {
let box = Box(wrappedValue: "ABC")
_foo = box
self.upperCased = box.wrappedValue.uppercased()
}
}

That is quite good (I mean, it works with no side effects, but is ugly).

The problem of this solution is that it doesn't really work (without side effects) in case your property wrapper has an empty initializer init() or if the wrappedValue is Optional.

If, for example, you try with the code below, you will realize that Box is initialized twice: once at the definition of the member variable and once in the OhNoes' init, and will replace the former.


@propertyWrapper
struct Box<Value> {
private var box: Value?

init(wrappedValue: Value?) { // Actually called twice in this case
box = wrappedValue
}

var wrappedValue: Value? {
get { box }
set { box = newValue }
}
}

class OhNoes {
@Box var foo : String?
let upperCased: String?

init() {
let box = Box(wrappedValue: "ABC")
_foo = box
self.upperCased = box.wrappedValue?.uppercased()
}
}

I think this is definitely something we shouldn't have, (or at least we should be able to opt out of this behavior). Anyway I think it's related to what they say in this pitch:

When a property wrapper type has a no-parameter init(), properties
that use that wrapper type will be implicitly initialized via init().

PS: did you find some other way of doing this?

Error Using property Wrapper in class swift 5.1

Use something like this:

@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T

init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}

var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}

https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md

How to subclass the @State property wrapper in SwiftUI

You can't subclass @State since @State is a Struct. You are trying to manipulate your model, so you shouldn't put this logic in your view. You should at least rely on your view model this way:

class ContentViewModel: ObservableObject {
@Published var positiveInt = 0 {
didSet {
if positiveInt < 0 {
positiveInt = 0
}
}
}
}

struct ContentView: View {
@ObservedObject var contentViewModel = ContentViewModel()

var body: some View {
VStack {
Text("\(contentViewModel.positiveInt)")
Button(action: {
self.contentViewModel.positiveInt = -98
}, label: {
Text("TAP ME!")
})
}
}
}

But since SwiftuUI is not an event-driven framework (it's all about data, model, binding and so forth) we should get used not to react to events, but instead design our view to be "always consistent with the model". In your example and in my answer here above we are reacting to the integer changing overriding its value and forcing the view to be created again. A better solution might be something like:

class ContentViewModel: ObservableObject {
@Published var number = 0
}

struct ContentView: View {
@ObservedObject var contentViewModel = ContentViewModel()

private var positiveInt: Int {
contentViewModel.number < 0 ? 0 : contentViewModel.number
}

var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.contentViewModel.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}

Or even simpler (since basically there's no more logic):

struct ContentView: View {
@State private var number = 0

private var positiveInt: Int {
number < 0 ? 0 : number
}

var body: some View {
VStack {
Text("\(positiveInt)")
Button(action: {
self.number = -98
}, label: {
Text("TAP ME!")
})
}
}
}


Related Topics



Leave a reply



Submit