Reputation: 13246
I would like to reset some @State
each time an @ObservedObject
is "reassigned". How can this be accomplished?
class Selection: ObservableObject {
let id = UUID()
}
struct ContentView: View {
@State var selectedIndex: Int = 0
var selections: [Selection] = [Selection(), Selection(), Selection()]
var body: some View {
VStack {
// Assume the user can select something so selectedIndex changes all the time
// SwiftUI will just keep updating the same presentation layer with new data
Button("Next") {
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
}
SelectionDisplayer(selection: self.selections[self.selectedIndex])
}
}
}
struct SelectionDisplayer: View {
@ObservedObject var selection: Selection {
didSet { // Wish there were something *like* this
print("will never happen")
self.tabIndex = 0
}
}
@State var tapCount: Int = 0 // Reset this to 0 when `selection` changes from one object to another
var body: some View {
Text(self.selection.id.description)
.onReceive(self.selection.objectWillChange) {
print("will never happen")
}
Button("Tap Count: \(self.tapCount)") {
self.tapCount += 1
}
}
}
I'm aware of onReceive
but I'm not looking to modify state in response to objectWillChange
but rather when the object itself is switched out. In UIKit with reference types I would use didSet
but that doesn't work here.
I did try using a PreferenceKey
for this (gist) but it seems like too much of a hack.
Upvotes: 2
Views: 8171
Reputation: 13246
Currently (Beta 5), the best way may be to use the constructor plus a generic ObservableObject
for items that I want to reset when the data changes. This allows some @State
to be preserved, while some is reset.
In the example below, tapCount
is reset each time selection
changes, while allTimeTaps
is not.
class StateHolder<Value>: ObservableObject {
@Published var value: Value
init(value: Value) {
self.value = value
}
}
struct ContentView: View {
@State var selectedIndex: Int = 0
var selections: [Selection] = [Selection(), Selection(), Selection()]
var body: some View {
VStack {
// Assume the user can select something so selectedIndex changes all the time
// SwiftUI will just keep updating the same presentation though
Button("Next") {
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
}
SelectionDisplayer(selection: self.selections[self.selectedIndex])
}
}
}
struct SelectionDisplayer: View {
struct SelectionState {
var tapCount: Int = 0
}
@ObservedObject var selection: Selection
@ObservedObject var stateHolder: StateHolder<SelectionState>
@State var allTimeTaps: Int = 0
init(selection: Selection) {
let state = SelectionState()
self.stateHolder = StateHolder(value: state)
self.selection = selection
}
var body: some View {
VStack {
Text(self.selection.id.description)
Text("All Time Taps: \(self.allTimeTaps)")
Text("Tap Count: \(self.stateHolder.value.tapCount)")
Button("Tap") {
self.stateHolder.value.tapCount += 1
self.allTimeTaps += 1
}
}
}
}
While looking for a solution I was quite interested to discover that you cannot initialize @State
variables in init
. The compiler will complain that all properties have not yet been set before accessing self.
Upvotes: 3
Reputation: 1184
This does what you want I believe:
class Selection: ObservableObject { let id = UUID() }
struct ContentView: View {
@State var selectedIndex: Int = 0
var selections: [Selection] = [Selection(), Selection(), Selection()]
@State var count = 0
var body: some View {
VStack {
Button("Next") {
self.count = 0
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
}
SelectionDisplayer(selection: self.selections[self.selectedIndex], count: $count)
}
}
}
struct SelectionDisplayer: View {
@ObservedObject var selection: Selection
@Binding var count: Int
var body: some View {
VStack {
Text("\(self.selection.id)")
Button("Tap Count: \(self.count)") { self.count += 1 }
}
}
}
My Xcode didn't like your code so I needed to make a few other changes than just moving the count into the parent
Upvotes: 0
Reputation: 5348
The only way to get this done for me was to force the parent view to redraw the child by temporarily hiding it. It's a hack, but the alternative was to pass a $tapCount
in, which is worse since then the parent does not only have to know that it has to be redrawn, but must also know of state inside.
This can probably be refactored into it's own view, which make it not as dirty.
import Foundation
import SwiftUI
import Combine
class Selection {
let id = UUID()
}
struct RedirectStateChangeView: View {
@State var selectedIndex: Int = 0
@State var isDisabled = false
var selections: [Selection] = [Selection(), Selection(), Selection()]
var body: some View {
VStack {
// Assume the user can select something so selectedIndex changes all the time
// SwiftUI will just keep updating the same presentation layer with new data
Button("Next") {
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
self.isDisabled = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.005) {
self.isDisabled = false
}
}
if !isDisabled {
SelectionDisplayer(selection: selections[selectedIndex])
}
}
}
}
struct SelectionDisplayer: View {
var selection: Selection
@State var tapCount: Int = 0 // Reset this to 0 when `selection` changes from one object to another
var body: some View {
VStack{
Text(selection.id.description)
Button("Tap Count: \(self.tapCount)") {
self.tapCount += 1
}
}
}
}
Upvotes: 1