在 SwiftUI 中处理派生状态 [英] Handling derived state in SwiftUI

查看:22
本文介绍了在 SwiftUI 中处理派生状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

说我正在创建一个日期编辑器"视图.目标是:- 采用默认的种子日期.- 它让用户改变输入.- 如果用户选择,他们可以按保存",在这种情况下,视图的所有者可以决定对数据做一些事情.

say I am creating an "Date Editor" view. The goal is: - Take a default, seed date. - It lets the user alter the input. - If the user then chooses, they can press "Save", in which case the owner of the view can decide to do something with the data.

这是实现它的一种方法:

Here's one way to implement it:

struct AlarmEditor : View {
    var seedDate : Date
    var handleSave : (Date) -> Void

    @State var editingDate : Date?

    var body : some View {
        let dateBinding : Binding<Date> = Binding(
            get: {
                return self.editingDate ?? seedDate
            },
            set: { date in
                self.editingDate = date
            }
        )

        return VStack {
            DatePicker(
                selection: dateBinding,
                displayedComponents: .hourAndMinute,
                label: { Text("Date") }
            )
            Spacer()
            Button(action: {
                self.handleSave(dateBinding.wrappedValue)
            }) {
                Text("Save").font(.headline).bold()
            }
        }
    }
}

问题

如果所有者更改了 seedDate 的值怎么办?

What if the owner changes the value of seedDate?

假设在这种情况下,我想要做的是将 editingDate 的值重置为新的 seedDate.

Say in that case, what I wanted to do was to reset the value of editingDate to the new seedDate.

这样做的惯用方式是什么?

What would be an idiomatic way of doing this?

推荐答案

评论和警告

基本上,这个问题相当于寻找替代品 用于 OP 的 var seedDate 上的 didSet.

Comment and warning

Basically, this question amounts to looking for a replacement for didSet on the OP's var seedDate.

几个月前,我就同一个问题向 Apple 提出了一项支持请求.他们的最新回应是他们收到了几个这样的问题,但他们还没有一个好的"解决方案.我在下面分享了解决方案,他们回答说既然有效,就使用它."

I used one of my support requests with Apple on this same question a few months ago. The latest response from them was that they have received several questions like this, but they don't have a "good" solution yet. I shared the solution below and they answered "Since it's working, use it."

下面的内容很臭",但确实有效.希望我们会在 iOS 14 中看到改进,从而消除此类操作的必要性.

What follows below is quite "smelly" but it does work. Hopefully we'll see improvements in iOS 14 that remove the necessity for something like this.

我们可以利用 body 是视图渲染的唯一入口这一事实.因此,我们可以跟踪视图输入随时间的变化,并基于此更改内部状态.我们只需要注意我们如何更新事物,以便 SwiftUI 的 State 想法不会被错误地修改.

We can take advantage of the fact that body is the only entrance point for view rendering. Therefore, we can track changes to our view's inputs over time and change internal state based on that. We just have to be careful about how we update things so that SwiftUI's idea of State is not modified incorrectly.

我们可以通过使用包含两个引用值的 struct 来做到这一点:

We can do this by using a struct that contains two reference values:

  1. 我们要跟踪的值
  2. 当#1 改变时我们想要修改的值

如果我们希望 SwiftUI 更新,我们将替换参考值.如果我们想根据 body 内 #1 的变化进行更新,我们更新 引用值所持有的值.

If we want SwiftUI to update we replace the reference value. If we want to update based on changes to #1 inside the body, we update the value held by the reference value.

要点在这里

首先,我们想将任何值包装在引用类型中.这允许我们在不触发 SwiftUI 的更新机制的情况下保存值.

First, we want to wrap any value in a reference type. This allows us to save a value without triggering SwiftUI's update mechanisms.

// A class that lets us wrap any value in a reference type
class ValueHolder<Value> {
    init(_ value: Value) { self.value = value }
    var value: Value
}

现在,如果我们声明 @State var valueHolder = ValueHolder(0) 我们可以这样做:

Now, if we declare @State var valueHolder = ValueHolder(0) we can do:

Button("Tap me") {
  self.valueHolder.value = 0 // **Doesn't** trigger SwiftUI update
  self.valueHolder = ValueHolder(0) // **Does** trigger SwiftUI update
}

其次,创建一个属性包装器,其中包含其中两个,一个用于我们的外部输入值,一个用于我们的内部状态.

Second, create a property wrapper that holds two of these, one for our external input value, and one for our internal state.

这个答案 解释我为什么在属性包装器中使用 State.

See this answer for an explanation of why I use State in the property wrapper.

// A property wrapper that holds a tracked value, and a value we'd like to update when that value changes.
@propertyWrapper
struct TrackedValue<Tracked, Value>: DynamicProperty {
    var trackedHolder: State<ValueHolder<Tracked>>
    var valueHolder: State<ValueHolder<Value>>

    init(wrappedValue value: Value, tracked: Tracked) {
        self.trackedHolder = State(initialValue: ValueHolder(tracked))
        self.valueHolder = State(initialValue: ValueHolder(value))
    }

    var wrappedValue: Value {
        get { self.valueHolder.wrappedValue.value }
        nonmutating set { self.valueHolder.wrappedValue = ValueHolder(newValue) }
    }

    var projectedValue: Self { return self }
}

最后添加一个方便的方法,让我们在需要的时候高效更新.由于这会返回一个 View,您可以在任何 ViewBuilder 中使用它.

And finally add a convenience method to let us efficiently update when we need to. Since this returns a View you can use it inside of any ViewBuilder.

extension TrackedValue {
    @discardableResult
    public func update(tracked: Tracked, with block:(Tracked, Value) -> Value) -> some View {
        self.valueHolder.wrappedValue.value = block(self.trackedHolder.wrappedValue.value, self.valueHolder.wrappedValue.value)
        self.trackedHolder.wrappedValue.value = tracked
        return EmptyView()
    }
}

用法

如果你运行下面的代码,每次masterCount改变时childCount都会重置为0.

struct ContentView: View {
    @State var count: Int = 0

    var body: some View {
        VStack {
            Button("Master Count: \(self.count)") {
                self.count += 1
            }
            ChildView(masterCount: self.count)
        }
    }
}

struct ChildView: View {
    var masterCount: Int
    @TrackedValue(tracked: 0) var childCount: Int = 0

    var body: some View {
        self.$childCount.update(tracked: self.masterCount) { (old, myCount) -> Int in
            if self.masterCount != old {
                return 0
            }
            return myCount
        }
     return Button("Child Count: \(self.childCount)") {
            self.childCount += 1
        }
    }
}

这篇关于在 SwiftUI 中处理派生状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆