@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
Swift: 'Hashable.Hashvalue' Is Deprecated as a Protocol Requirement;
String Convert to Int and Replace Comma to Plus Sign
Swift 3: the Difference Between Public and Internal Access Modifiers
Swift 2: Expression Pattern of Type 'Bool' Cannot Match Values of Type 'Int'
Concatenate Two Audio Files in Swift and Play Them
Swift & Firebase | Checking If a User Exists with a Username
What's the How to Use an Objective-C Category Within Swift
Build Error When Trying to Override an Initializer in Xcode 6.3 Beta 3
Download File from Server Using Swift
How We Can Get and Read Size of a Text with Geometryreader in Swiftui
How to Detect a Swiftui Touchdown Event with No Movement or Duration
Why Does Filter(_:)'s Predicate Get Called So Many Times When Evaluating It Lazily
Error When Trying to Save Image in Nsuserdefaults Using Swift
Manage Ifaddrs to Return MAC Addresses as Well in Swift