当状态改变时,如何运行操作?

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)")) ?我看了所有的文件,什么都没找到。

31398 次浏览

Not really answering your question, but here's the right way to set up SegmentedControl (didn't want to post that code as a comment, because it looks ugly). Replace your ForEach version with the following code:

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

Tagging views with enumeration cases or even strings makes it behave inadequately – selection doesn't work.

You might also want to add the following after the SegmentedControl declaration to ensure that selection works:

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

Full version of 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)")
}
}

Regarding your question – I tried adding didSet observer to selection, but it crashes Xcode editor and generates "Segmentation fault: 11" error when trying to build.

You can't use didSet observer on @State but you can on an ObservableObject property.

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

Then use it like this:

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

Here is another option if you have a component that updates a @Binding. Rather than doing this:

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

you can do this and have a little greater control:

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

This way you get the benefits of the binding along with the detection of when the Component has changed the value.

In iOS 14 there is now a onChange modifier you can use like so:

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+

You can use the onChange(of:perform:) modifier, like so:

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+

The following as an extension of Binding, so you can execute a closure whenever the value changes.

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

Used like so for example:

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

This example doesn't need to use a separate function. You only need a closure.

iOS 13+

You can use 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)
}
}
}

You can use Binding

let textBinding = Binding<String>(
get: { /* get */ },
set: { /* set $0 */ }
)

I like to solve this by moving the data into a struct:

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

The advantage this way is if you decide to update another var in the struct based on the new value in didSet, and if you make your binding animated, e.g. isOn: $data.isLightOn.animation() then any Views you update that use the other var will animate their change during the toggle. That doesn't happen if you use onChange.

E.g. here the list sort order change animates:

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