17

我正在尝试创建 SwiftUI 支持的自定义属性包装器,这意味着对相应属性值的更改将导致 SwiftUI 视图的更新。这是我所拥有的简化版本:

@propertyWrapper
public struct Foo: DynamicProperty {
    @ObservedObject var observed: SomeObservedObject

    public var wrappedValue: [SomeValue] {
        return observed.value
    }
}

我看到即使 myObservedObject包含在我的自定义属性包装器中,只要满足以下条件,SwiftUI 仍然会捕获更改SomeObservedObject

  • 我的属性包装器是一个结构
  • 我的属性包装器符合DynamicProperty

不幸的是,文档很少,我很难判断这是否仅适用于当前的 SwiftUI 实现。

(在 Xcode 中,而不是在线)的文档DynamicProperty似乎表明这样的属性是从外部更改的属性,导致视图重绘,但是不能保证当您使自己的类型符合该协议时会发生什么。

我可以期望这在未来的 SwiftUI 版本中继续工作吗?

4

4 回答 4

15

好的......这是获得类似东西的替代方法......但作为结构仅DynamicProperty环绕@State(强制视图刷新)。

它是一个简单的包装器,但可以通过以下视图刷新来封装任何自定义计算……正如所说的使用纯值类型。

这是演示(使用 Xcode 11.2 / iOS 13.2 测试):

DynamicProperty 作为@State 上的包装器

这是代码:

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
            }
        }
    }
}

struct TestPropertyWrapper_Previews: PreviewProvider {
    static var previews: some View {
        TestPropertyWrapper()
    }
}
于 2020-01-07T05:56:08.860 回答
1

对于你标题中的问题,没有。例如,下面是一些不起作用的代码:

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
        }
    }
}

因此,显然您仍然需要在您的内部放置一个StateObservedObjectDynamicProperty来触发更新,就像 Asperi 所做的那样,aDynamicProperty本身并不强制执行任何更新。

于 2021-01-27T02:20:24.993 回答
0

您可以使用与SwiftUI 中@propertyWrapper的对象相同的方式创建和使用 Combine 发布者。@Published

通过将您的发布者传递@porpertyWrapper给 a projectedValue,您将拥有一个自定义的 Combine 发布者,您可以在 SwiftUI 视图中使用它并调用$来跟踪值随时间的变化。

在 SwiftUI 视图或视图模型中使用:

@Foo(defaultValue: "foo") var value: String

// For your view model or SwiftUI View
$value

您的完整自定义组合发布者为@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()
  }
}
于 2021-08-31T20:27:18.127 回答
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
        }
    }
}

当一个 View using@Foo被重新创建时,SwiftUI 将与上次相同的对象传递给 Foo 结构,因此具有相同的计数器值。设置 foo var 时,这是在SwiftUI 检测为更改并导致重新计算主体的ObservableObject's上设置的。@Published

试试看!

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的主体被调用并被ContentView2()重新创建,但保持与上次相同的计数器。例如,现在SomeObservedObject可以是一个NSObject并实现一个delegate协议。

于 2020-12-04T14:00:14.483 回答