Как я могу выполнить действие при изменении состояния?

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 состояние меняется? Я просмотрел документы и ничего не смог найти.

9 ответов

Решение

Вы не можете использовать didSet наблюдатель на @State но вы можете на 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)
            // }
        }
    }
}

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 (iOS 13 и 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)
            }
    }
}

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

Вот еще один вариант, если у вас есть компонент, который обновляет @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 17+

Оно снова изменилось! Замыкание с одним параметром теперь выдает следующее предупреждение:

«onChange(of:perform:)» устарел в iOS 17.0: используйтеonChangeвместо этого с замыканием действия с двумя или нулевыми параметрами.

Итак, теперь вы пишете это одним из двух способов:

  1. Путь с нулевым параметром.
  2. 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 .

Вы можете использовать привязку

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

Не совсем отвечаю на ваш вопрос, но вот правильный способ настроить 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)")
    }
}

По вашему вопросу - я попытался добавить didSet наблюдатель selection, но он аварийно завершает работу редактора Xcode и выдает ошибку "Ошибка сегментации: 11" при попытке сборки.

Мне нравится решать эту проблему, перемещая данные в структуру:

      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()
    }
}
Другие вопросы по тегам