Как я могу выполнить действие при изменении состояния?
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
вместо этого с замыканием действия с двумя или нулевыми параметрами.
Итак, теперь вы пишете это одним из двух способов:
- Путь с нулевым параметром.
- 2-параметрический способ.
Нулевой параметр
.onChange(of: playState) {
model.playStateDidChange(state: playState)
}
2-параметрический
.onChange(of: playState) { oldState, newState in
model.playStateDidChange(from: oldState, to: newState)
}
Вы можете использовать привязку
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()
}
}