当状态改变时,我如何运行一个动作?

86
enum SectionType: String, CaseIterable {
    case top = "Top"
    case best = "Best"
}

struct ContentView : View {
    @State private var selection: Int = 0

    var body: some View {
        SegmentedControl(selection: $selection) {
            ForEach(SectionType.allCases.identified(by: \.self)) { type in
                Text(type.rawValue).tag(type)
            }
        }
    }
}

$selection状态更改时,我该如何运行代码(例如print("Selection changed to \(selection)"))? 我查看了文档,但没有找到相关内容。


1
抱歉之前我的回答是错误的。我已经将其删除了,但我正在调查此事,一旦找到答案就会回复您 :D - Fogmeister
1
我还没有足够的经验来确定 - 如果完全错误,我会很乐意删除这个评论 - 但是我尝试执行一个带有printfunc,甚至设置了断点,但没有成功。然而,当我放置SwiftUI代码(例如创建一个Text(“got here”))时,它可以工作。考虑到这一切都是beta1,我认为这将在以后的beta版本中得到纠正(或至少更好地记录)。 - user7014451
1
你可能想尝试使用@Published修饰符并订阅它。 - Daniele Bernardini
1
这是Combine框架,请查看WWDC上有关Combine的视频。 - Daniele Bernardini
@DanieleBernardini 谢谢,我会看一下。 - user7885981
显示剩余3条评论
9个回答

82

iOS 14.0+

您可以使用onChange(of:perform:)修饰符,如下所示:

struct ContentView: View {
    
    @State private var isLightOn = false

    var body: some View {
        Toggle("Light", isOn: $isLightOn)
            .onChange(of: isLightOn) { value in
                if value {
                    print("Light is now on!")
                } else {
                    print("Light is now off.")
                }
            }
    }
}

iOS 13.0+

下面是Binding的扩展,因此您可以在值更改时执行闭包。

extension Binding {
    
    /// When the `Binding`'s `wrappedValue` changes, the given closure is executed.
    /// - Parameter closure: Chunk of code to execute whenever the value changes.
    /// - Returns: New `Binding`.
    func onUpdate(_ closure: @escaping () -> Void) -> Binding<Value> {
        Binding(get: {
            wrappedValue
        }, set: { newValue in
            wrappedValue = newValue
            closure()
        })
    }
}

比如,可以这样使用:

struct ContentView: View {
    
    @State private var isLightOn = false
    
    var body: some View {
        Toggle("Light", isOn: $isLightOn.onUpdate(printInfo))
    }
    
    private func printInfo() {
        if isLightOn {
            print("Light is now on!")
        } else {
            print("Light is now off.")
        }
    }
}

这个例子不需要使用单独的函数,你只需要一个闭包。


如果您在切换绑定中使用 $isLightOn.animation(),则 onChange 中的副作用不会像通常情况下那样进行动画处理,因此我认为这不是正确的方法。 - malhal
这对我来说是最好的方法。谢谢。 - Vladimir Amiorkov

52

你不能在@State上使用didSet观察器,但是你可以在ObservableObject属性上使用。

import SwiftUI
import Combine

final class SelectionStore: ObservableObject {
    var selection: SectionType = .top {
        didSet {
            print("Selection changed to \(selection)")
        }
    }

    // @Published var items = ["Jane Doe", "John Doe", "Bob"]
}

然后像这样使用它:

import SwiftUI

enum SectionType: String, CaseIterable {
    case top = "Top"
    case best = "Best"
}

struct ContentView : View {
    @ObservedObject var store = SelectionStore()

    var body: some View {
        List {
            Picker("Selection", selection: $store.selection) {
                ForEach(FeedType.allCases, id: \.self) { type in
                    Text(type.rawValue).tag(type)
                }
            }.pickerStyle(SegmentedPickerStyle())

            // ForEach(store.items) { item in
            //     Text(item)
            // }
        }
    }
}

4
你也可以使用 onReceive - onmyway133
如果我想在ContentView的didSet中更改另一个@State属性怎么办?使用您的代码,我卡在了SelectionStore实例中。 - Lupurus
1
你可以在简单的结构体上重写didSet,无需使用对象。 - malhal

25

iOS 13+

您可以使用 onReceive

import Combine
import SwiftUI

struct ContentView: View {
    @State private var selection = false

    var body: some View {
        Toggle("Selection", isOn: $selection)
            .onReceive(Just(selection)) { selection in
                // print(selection)
            }
    }
}

1
你用那个“Just”救了我一命。我已经找了很久了。我需要在一个@State变量上延迟,而有了那个“Just”,现在这变得非常简单且正常工作。非常感谢你! - undefined

25
在iOS 14中,现在有一个onChange修饰符,您可以像这样使用:
SegmentedControl(selection: $selection) {
    ForEach(SectionType.allCases.identified(by: \.self)) { type in
        Text(type.rawValue).tag(type)
    }
}
.onChange(of: selection) { value in
    print("Selection changed to \(selection)")
}

23

如果您有一个更新@Binding的组件,这里还有另一种选择。而不是这样做:

这里有另一个选择,如果您有一个更新@Binding的组件。而不是这样做:

Component(selectedValue: self.$item, ...)

你可以这样做,并且拥有更大的控制权:

Component(selectedValue: Binding(
    get: { self.item },
    set: { (newValue) in
              self.item = newValue
              // now do whatever you need to do once this has changed
    }), ... )

这样您就可以同时获得绑定的好处和检测 Component 更改值的功能。


这会抛出运行时错误:“在视图更新期间修改状态,这将导致未定义的行为。” - Wil Gieseler
喜欢这个答案。这种模式已经救了我几次了。 - J-Krush
对于那些想要在Binding上进行extension的人,请查看下面链接:below。我认为这样调用更加整洁。 - George

3
你可以使用绑定。
let textBinding = Binding<String>(
    get: { /* get */ },
    set: { /* set $0 */ }
)

2

iOS 17+

又有变动了!现在,单参数闭包会出现以下警告:

'onChange(of:perform:)' 在 iOS 17.0 中已被弃用:请改用带有两个或零个参数的 onChange 闭包。

因此,现在有两种写法:

  1. 零参数写法。
  2. 两参数写法。

零参数写法

.onChange(of: playState) {
            model.playStateDidChange(state: playState)
        }

查看https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-8wgw9?ref=optimistic-closures.com

2个参数

.onChange(of: playState) { oldState, newState in
            model.playStateDidChange(from: oldState, to: newState)
        }

查看https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-4psgg?ref=optimistic-closures.com

1

虽然我没有回答你的问题,但是下面是正确设置SegmentedControl的方法(我不想把代码作为评论发布,因为它看起来很丑)。请使用以下代码替换您的ForEach版本:

ForEach(0..<SectionType.allCases.count) { index in 
    Text(SectionType.allCases[index].rawValue).tag(index)
}

将视图标记为枚举案例甚至字符串会导致其行为不当-选择无法正常工作。

您可能还想在SegmentedControl声明之后添加以下内容,以确保选择工作:

Text("Value: \(SectionType.allCases[self.selection].rawValue)")

body的完整版本:

var body: some View {
    VStack {
        SegmentedControl(selection: self.selection) {
            ForEach(0..<SectionType.allCases.count) { index in
                Text(SectionType.allCases[index].rawValue).tag(index)
                }
            }

        Text("Value: \(SectionType.allCases[self.selection].rawValue)")
    }
}

关于你的问题 - 我尝试为 selection 添加 didSet 观察器,但它会导致 Xcode 编辑器崩溃,并在尝试构建时生成 "Segmentation fault: 11" 错误。


0

我喜欢通过将数据移入结构体来解决这个问题:

struct ContentData {
    var isLightOn = false {
        didSet {
            if isLightOn {
                print("Light is now on!")
            } else {
                print("Light is now off.")
            }
            // you could update another var in this struct based on this value
        }
    }
}

struct ContentView: View {
    
    @State private var data = ContentData()

    var body: some View {
        Toggle("Light", isOn: $data.isLightOn)
    }
}

这种方式的优点是,如果您决定根据didSet中的新值更新结构体中的另一个变量,并且如果您使绑定动画化,例如isOn: $data.isLightOn.animation(),那么使用其他变量的任何视图在切换期间都会动画显示其更改。如果您使用onChange,则不会发生这种情况。

例如,在此处,列表排序顺序更改会进行动画处理:

import SwiftUI

struct ContentData {
    var ascending = true {
        didSet {
            sort()
        }
    }
    
    var colourNames = ["Red", "Green", "Blue", "Orange", "Yellow", "Black"]
    
    init() {
        sort()
    }
    
    mutating func sort(){
        if ascending {
            colourNames.sort()
        }else {
            colourNames.sort(by:>)
        }
    }
}


struct ContentView: View {
    @State var data = ContentData()
    
    var body: some View {
        VStack {
            Toggle("Sort", isOn:$data.ascending.animation())
            List(data.colourNames, id: \.self) { name in
                Text(name)
            }
        }
        .padding()
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接