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
Expression Implicitly Coerced from 'String' to Any
Adding Uibarbutton Item in Swift
Aws S3 Transfer Manager ${Cognito-Identity.Amazonaws.Com:Sub} Policy Variable Access Denied
How to Delete from Firebase Database
Attaching Audiounit Effects to Scnaudiosource Nodes in Scenekit
Is String Type a Class or a Struct? or Something Else
Reusing Security Scoped Bookmark
How to Present Different Navigation Title When Large Title Collapse
How to Check Object Is Nil or Not in Swift
How to Get User Input in Apple's Swift Language in a Command Line Tool
Delete All Characters After a Certain Character from a String in Swift
Key-Value Coding (Kvc) with Array/Dictionary in Swift
Firebase Sign Out Not Working in Swift
Nsbundle.Mainbundle().Urlforresource("Bach1", Withextension: "Jpg") Returning Null