英文:
Is it correct to expect internal updates of a SwiftUI DynamicProperty property wrapper to trigger a view update?
问题
I'm attempting to create a custom property wrapper supported by SwiftUI, meaning that changes to the corresponding properties values would cause an update to the SwiftUI view. Here is a simplified version of what I have:
@propertyWrapper
public struct Foo: DynamicProperty {
@ObservedObject var observed: SomeObservedObject
public var wrappedValue: [SomeValue] {
return observed.value
}
}
I see that even if my ObservedObject
is contained inside of my custom property wrapper, SwiftUI still catches the changes to SomeObservedObject
as long as:
- My property wrapper is a struct
- My property wrapper conforms to
DynamicProperty
Unfortunately the docs are sparse and I have a hard time telling if this only works out of luck with the current SwiftUI implementation.
The docs of DynamicProperty
(within Xcode, not online) seem to indicate that such a property is a property that is changed from the outside causing the view to redraw, but there is no guarantee about what happens when you conform your own types to this protocol.
Can I expect this to continue working in future SwiftUI releases?
英文:
I'm attempting to create a custom property wrapper supported by SwiftUI, meaning that changes to the corresponding properties values would cause an update to the SwiftUI view. Here is a simplified version of what I have:
@propertyWrapper
public struct Foo: DynamicProperty {
@ObservedObject var observed: SomeObservedObject
public var wrappedValue: [SomeValue] {
return observed.value
}
}
I see that even if my ObservedObject
is contained inside of my custom property wrapper, SwiftUI still catches the changes to SomeObservedObject
as long as:
- My property wrapper is a struct
- My property wrapper conforms to
DynamicProperty
Unfortunately the docs are sparse and I have a hard time telling if this only works out of luck with the current SwiftUI implementation.
The docs of DynamicProperty
(within Xcode, not online) seem to indicate that such a property is a property that is changed from the outside causing the view to redraw, but there is no guarantee about what happens when you conform your own types to this protocol.
Can I expect this to continue working in future SwiftUI releases?
答案1
得分: 17
以下是您提供的代码的翻译部分:
好的... 这是一个获取类似功能的替代方法... 但只有结构体 `DynamicProperty` 包装了 `@State`(以强制视图刷新)。
这是一个简单的包装器,但可以封装任何自定义计算,并在视图刷新后使用值类型。
这是演示(已在 Xcode 11.2 / iOS 13.2 中测试):
![DynamicProperty 作为 @State 的包装器][1]
[1]: https://i.stack.imgur.com/nfhTC.gif
以下是代码:
```swift
import SwiftUI
@propertyWrapper
struct Refreshing<Value>: DynamicProperty {
let storage: State<Value>
init(wrappedValue value: Value) {
self.storage = State<Value>(initialValue: value)
}
public var wrappedValue: Value {
get { storage.wrappedValue }
nonmutating set { self.process(newValue) }
}
public var projectedValue: Binding<Value> {
storage.projectedValue
}
private func process(_ value: Value) {
// 在这里执行一些操作或在后台队列中执行
DispatchQueue.main.async {
self.storage.wrappedValue = value
}
}
}
struct TestPropertyWrapper: View {
@Refreshing var counter: Int = 1
var body: some View {
VStack {
Text("Value: \(counter)")
Divider()
Button("Increase") {
self.counter += 1
}
}
}
}
请注意,代码中包含一些特殊字符(如 <
和 >
),在翻译时保留了原样。如果需要进一步的帮助或解释,请告诉我。
英文:
Ok... here is alternate approach to get similar thing... but as struct only DynamicProperty
wrapped around @State
(to force view refresh).
It is simple wrapper but gives possibility to incapsulate any custom calculations with following view refresh... and as said using value-only types.
Here is demo (tested with Xcode 11.2 / iOS 13.2):
Here is code:
import SwiftUI
@propertyWrapper
struct Refreshing<Value> : DynamicProperty {
let storage: State<Value>
init(wrappedValue value: Value) {
self.storage = State<Value>(initialValue: value)
}
public var wrappedValue: Value {
get { storage.wrappedValue }
nonmutating set { self.process(newValue) }
}
public var projectedValue: Binding<Value> {
storage.projectedValue
}
private func process(_ value: Value) {
// do some something here or in background queue
DispatchQueue.main.async {
self.storage.wrappedValue = value
}
}
}
struct TestPropertyWrapper: View {
@Refreshing var counter: Int = 1
var body: some View {
VStack {
Text("Value: \(counter)")
Divider()
Button("Increase") {
self.counter += 1
}
}
}
}
答案2
得分: 1
对于标题中的问题,答案是否定的。例如,这里有一些不起作用的代码:
class Storage {
var value = 0
}
@propertyWrapper
struct MyState: DynamicProperty {
var storage = Storage()
var wrappedValue: Int {
get { storage.value }
nonmutating set {
storage.value = newValue // 这部分不起作用
}
}
}
所以显然,你仍然需要在你的DynamicProperty
内部放置一个State
或ObservedObject
等来触发更新,就像Asperi所做的一样,DynamicProperty
本身不会强制进行任何更新。
英文:
For the question in your title, no. For example, here is some code that doesn't work:
class Storage {
var value = 0
}
@propertyWrapper
struct MyState: DynamicProperty {
var storage = Storage()
var wrappedValue: Int {
get { storage.value }
nonmutating set {
storage.value = newValue // This doesn't work
}
}
}
So apparently you still need to put a State
or ObservedObject
etc. inside your DynamicProperty
to trigger an update as Asperi did, a DynamicProperty
itself doesn't enforce any update.
答案3
得分: 1
你可以创建并使用一个与 SwiftUI 中的 @Published
对象相同的 Combine 发布者,用于你的 @propertyWrapper
。
通过将你的 @propertyWrapper
的发布者传递给 projectedValue
,你将拥有一个自定义的 Combine 发布者,可以在 SwiftUI 视图中使用,并调用 $
来跟踪值随时间的变化。
在你的 SwiftUI 视图或视图模型中的使用:
@Foo(defaultValue: "foo") var value: String
// 对于你的视图模型或 SwiftUI 视图
$value
你的完整自定义 Combine 发布者作为 @propertyWrapper
:
import Combine
@propertyWrapper
struct Foo<Value> {
var defaultValue: Value
// 使用 Combine 发布者来随时间投射值。
private let publisher = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get {
return defaultValue
}
set {
defaultValue = newValue
publisher.send(newValue)
}
}
// 使用 Combine 发布者来投射更新的值。
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
}
英文:
You can create and use a Combine publisher with your @propertyWrapper
the same way a @Published
object from SwiftUI would do.
By passing the publisher of your @porpertyWrapper
to a projectedValue
, you will have a custom Combine publisher that you can use within your SwiftUI view and call the $
to keep track of the value changes over time.
The use in your SwiftUI view or inside a view model:
@Foo(defaultValue: "foo") var value: String
// For your view model or SwiftUI View
$value
Your full custom Combine publisher as a @propertyWrapper
:
import Combine
@propertyWrapper
struct Foo<Value> {
var defaultValue: Value
// Combine publisher to project value over time.
private let publisher = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get {
return defaultValue
}
set {
defaultValue = newValue
publisher.send(newValue)
}
}
// Project the updated value using the Combine publisher.
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
}
答案4
得分: 0
是的,这是正确的,这里有一个示例:
class SomeObservedObject: ObservableObject {
@Published var counter = 0
}
@propertyWrapper struct Foo: DynamicProperty {
@StateObject var object = SomeObservedObject()
public var wrappedValue: Int {
get {
object.counter
}
nonmutating set {
object.counter = newValue
}
}
}
当使用@Foo
的视图被重新创建时,SwiftUI会将相同的对象传递给Foo结构体,因此具有相同的计数器值。在设置foo变量时,这将设置在ObservableObject的@Published属性上,SwiftUI检测到这是一个更改,并导致重新计算body。
尝试一下!
struct ContentView: View {
@State var counter = 0
var body: some View {
VStack {
Text("\(counter) Hello, world!")
Button("Increment") {
counter = counter + 1
}
ContentView2()
}
.padding()
}
}
struct ContentView2: View {
@Foo var foo
var body: some View {
VStack {
Text("\(foo) Hello, world!")
Button("Increment") {
foo = foo + 1
}
}
.padding()
}
}
当点击第二个按钮时,存储在Foo
中的计数器会更新。当点击第一个按钮时,会调用ContentView
的body,并且ContentView2()
会被重新创建,但保留与上次相同的计数器。现在,SomeObservedObject
可以是NSObject
并实现一个delegate
协议,例如。
英文:
Yes this is correct, here is an example:
class SomeObservedObject : ObservableObject {
@Published var counter = 0
}
@propertyWrapper struct Foo: DynamicProperty {
@StateObject var object = SomeObservedObject()
public var wrappedValue: Int {
get {
object.counter
}
nonmutating set {
object.counter = newValue
}
}
}
When a View using @Foo
is recreated, SwiftUI passes the Foo struct the same object as last time, so has the same counter value. When setting the foo var, this is set on the ObservableObject
's @Published
which SwiftUI detects as a change and causes the body to be recomputed.
Try it out!
struct ContentView: View {
@State var counter = 0
var body: some View {
VStack {
Text("\(counter) Hello, world!")
Button("Increment") {
counter = counter + 1
}
ContentView2()
}
.padding()
}
}
struct ContentView2: View {
@Foo var foo
var body: some View {
VStack {
Text("\(foo) Hello, world!")
Button("Increment") {
foo = foo + 1
}
}
.padding()
}
}
When the second button is tapped the counter stored in Foo
is updated. When first button is tapped, ContentView
's body is called and ContentView2()
is recreated but keeps the same counter as last time. Now SomeObservedObject
can be a NSObject
and implement a delegate
protocol for example.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论