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

How can I run an action when a state changes?

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

我如何 运行 编码(例如 print("Selection changed to \(selection)")$selection 状态改变时?我查看了文档但找不到任何东西。

并没有真正回答您的问题,但这是设置 SegmentedControl 的正确方法(不想 post 该代码作为注释,因为它看起来很丑)。用以下代码替换您的 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)")
    }
}

关于您的问题 – 我尝试将 didSet 观察器添加到 selection,但它使 Xcode 编辑器崩溃并在尝试构建时生成 "Segmentation fault: 11" 错误。

您不能在 @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)
            // }
        }
    }
}

如果您有更新 @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 何时更改了值。

在 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)")
}

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.")
        }
    }
}

此示例不需要使用单独的函数。你只需要一个闭包。

SwiftUI 1 和 2(iOS13 和 14)

您可以使用 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)
            }
    }
}

您可以使用绑定

let textBinding = Binding<String>(
    get: { /* get */ },
    set: { /* set [=10=] */ }
)

我想通过将数据移动到结构中来解决这个问题:

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 中的新值更新结构中的另一个 var,并且如果您使绑定动画化,例如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()
    }
}