Trying to implement a custom property wrapper which would also publish its changes the same way @Publish
does.
E.g. allow my SwiftUI to receive changes on my property using my custom wrapper.
The working code I have:
import SwiftUI
@propertyWrapper
struct MyWrapper<Value> {
var value: Value
init(wrappedValue: Value) { value = wrappedValue }
var wrappedValue: Value {
get { value }
set { value = newValue }
}
}
class MySettings: ObservableObject {
@MyWrapper
public var interval: Double = 50 {
willSet { objectWillChange.send() }
}
}
struct MyView: View {
@EnvironmentObject var settings: MySettings
var body: some View {
VStack() {
Text("\(settings.interval, specifier: "%.0f")").font(.title)
Slider(value: $settings.interval, in: 0...100, step: 10)
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView().environmentObject(MySettings())
}
}
However, I do not like the need to call objectWillChange.send()
for every property in MySettings
class.
The @Published
wrapper works well, so I tried to implement it as part of @MyWrapper
, but I was not successful.
A nice inspiration I found was https://github.com/broadwaylamb/OpenCombine, but I failed even when trying to use the code from there.
When struggling with the implementation,
I realised that in order to get @MyWrapper
working I need to precisely understand how @EnvironmentObject
and @ObservedObject
subscribe to changes of @Published
.
Any help would be appreciated.
SwiftUI has two properties wrappers for reading the user's environment: @Environment and @ScaledMetric . @Environment is used to read a wide variety of data such as what trait collection is currently active, whether they are using a 2x or 3x screen, what timezone they are on, and more.
The @State property wrapper is used inside of View objects and allows your view to respond to any changes made to @State . You use @State for properties that are owned by the view that it's contained in. In other words, a view initializes its @State properties itself.
@Published is one of the property wrappers in SwiftUI that allows us to trigger a view redraw whenever changes occur. You can use the wrapper combined with the ObservableObject protocol, but you can also use it within regular classes.
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())
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With