Is It Correct to Expect Internal Updates of a Swiftui Dynamicproperty Property Wrapper to Trigger a View Update

@propertywrapper in SwiftUI

@Asperi is exactly right as to how to create the property wrapper. However, that does not solve the problem of the TextField(). The issue seems to be that You are not actually using $quantity on your TextField, but rather are using a string from the formatter that is derived from $quantity. That just doesn't seem to allow the update mechanism to work properly.

However, you can fix this simply by feeding a @State string into the TextField, and then updating everything in an .onChange(of:). This allows you to set quantity to the Int value of the TextField, and the maximum prevents the quantity from going too high. You then turn around and set your string to the quantity.description to keep everything in sync.

One last thing, I changed the keyboardType to .decimalPad to make inputting easier.

struct ContentView: View {

@Maximum(maximum: 12) var quantity: Int
@State private var qtyString = "0"

var body: some View {
NavigationView{
Form{
TextField("", text: $qtyString, prompt: Text("Pizza").foregroundColor(.red))
.onChange(of: qtyString) { newValue in
if let newInt = Int(newValue) {
quantity = newInt
qtyString = quantity.description
}
}
.keyboardType(.decimalPad)
Button {
quantity += 1
} label: {
Text("\(quantity)")
}
}
}
}
}

@propertyWrapper
struct Maximum<T: Comparable>: DynamicProperty where T: Numeric {
let number: State<T> = State(initialValue: 0)

var max: T

var wrappedValue: T {
get { number.wrappedValue }
nonmutating set { number.wrappedValue = min(newValue, max) }
}

var projectedValue: Binding<T> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}

init(maximum: T){
max = maximum
}
}

How to implement a custom property wrapper which would publish the changes for SwiftUI to re-render it's view

Until the https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type gets implemented, I came up with the solution below.

Generally, I pass the objectWillChange reference of the MySettings to all properties annotated with @MyWrapper using reflection.

import Cocoa
import Combine
import SwiftUI

protocol PublishedWrapper: class {
var objectWillChange: ObservableObjectPublisher? { get set }
}

@propertyWrapper
class MyWrapper<Value>: PublishedWrapper {
var value: Value
weak var objectWillChange: ObservableObjectPublisher?

init(wrappedValue: Value) { value = wrappedValue }

var wrappedValue: Value {
get { value }
set {
value = newValue
objectWillChange?.send()
}
}
}

class MySettings: ObservableObject {
@MyWrapper
public var interval1: Double = 10

@MyWrapper
public var interval2: Double = 20

/// Pass our `ObservableObjectPublisher` to the property wrappers so that they can announce changes
init() {
let mirror = Mirror(reflecting: self)
mirror.children.forEach { child in
if let observedProperty = child.value as? PublishedWrapper {
observedProperty.objectWillChange = self.objectWillChange
}
}
}
}

struct MyView: View {
@EnvironmentObject
private var settings: MySettings

var body: some View {
VStack() {
Text("\(settings.interval1, specifier: "%.0f")").font(.title)
Slider(value: $settings.interval1, in: 0...100, step: 10)

Text("\(settings.interval2, specifier: "%.0f")").font(.title)
Slider(value: $settings.interval2, in: 0...100, step: 10)
}
}
}

struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView().environmentObject(MySettings())
}
}

Custom Property Wrapper that Updates View Swift

The solution to this is to make a minor tweak to the solution of the singleton. Credits to @user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:

class Session: ObservableObject {

@DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []

private var cancellers = [AnyCancellable]()

private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}

static let current = Session()

}

When the @Published publishes the value it wraps?

.onReceive will be executed every time feedData is changed, which is when the Published publisher will emit a value.

If Feed is a value-type, like a struct, then anytime any of its properties change, the value-type semantics of Swift ensure that the entire object is being changed.

If Feed is a reference-type - a class, then only when setting feedData to a different instance would emit a value.

Passing binding to a variable of type property wrapper - loosing underlying type

Add the following extension with operator and your code worked. Tested with Xcode 12.4 / iOS 14.4

extension BoundedNumber {
static func +=(_ lhs: inout BoundedNumber, _ rhs: Int) {
lhs.wrappedValue += rhs
}
}

How to change @State property wrapper from nested view

To change the wrapper value in your DocumentPicker struct you can define a @Binding variable and pass your value to it, this toggle your variable on your parent view, but before showing the alert you need to dismiss the DocumentPicker



Related Topics



Leave a reply



Submit