How can I update state in SwiftUI on change of props (in React terminology)?
In React there is getDerivedStateFromProps(), in Flutter there is didUpdateWidget(). What is the SwiftUI equivalent?
Motivational example:
There is DiscoView which is just a hardcoded text that should blink with the given period.
There is a parent view which may change that period. When period changes, I need to update the timer somehow.
let colors: [Color] = [.red, .green, .black, .yellow, .pink, .blue]
struct DiscoView: View {
var period: TimeInterval
@State var color: Int = 0
@State var timer: Timer? = nil
var body: some View {
Text("DISCO")
.foregroundColor(colors[color])
.onAppear {
self.timer = Timer.scheduledTimer(withTimeInterval: self.period, repeats: true) { _ in
self.color = (self.color + 1) % colors.count
}
}
.onDisappear {
self.timer?.invalidate()
}
}
}
struct DiscoManager: View {
@State var period: TimeInterval = 1
var body: some View {
VStack {
DiscoView(period: self.period)
HStack {
Button("Slower") {
self.period *= 2
}
Button("Faster") {
self.period /= 2
}
}
}
}
}
gnperdue
(Gabriel Perdue)
2
I think you need a @Binding... however, my solution doesn't really seem to solve your problem as when I watch the colors, the blinking rate doesn't look different even though the period variable is definitely changing...
let colors: [Color] = [.red, .green, .black, .yellow, .pink, .blue]
struct DiscoView: View {
@Binding var period: TimeInterval
@State var color: Int = 0
@State var timer: Timer? = nil
var body: some View {
Text("DISCO - period: \(String(period))")
.foregroundColor(colors[color])
.onAppear {
self.timer = Timer.scheduledTimer(withTimeInterval: self.period, repeats: true) { _ in
self.color = (self.color + 1) % colors.count
}
}
.onDisappear {
self.timer?.invalidate()
}
}
}
struct DiscoManager: View {
@State var period: TimeInterval = 1
var body: some View {
VStack {
DiscoView(period: self.$period)
HStack {
Button("Slower") {
self.period *= 2
}
Button("Faster") {
self.period /= 2
}
}
}
}
}
Onne
(Onne van Dijk)
3
I'm circumventing the more general question, but in this case you could just do it like this, by making Timer a publisher:
let colors: [Color] = [.red, .green, .black, .yellow, .pink, .blue]
struct DiscoView: View {
var period: TimeInterval
@State var color: Int = 0
var body: some View {
Text("DISCO")
.foregroundColor(colors[color])
.onReceive(
Timer.publish(every: period, on: .main, in: .default).autoconnect()
) { _ in
self.color = (self.color + 1) % colors.count
}
}
}
struct DiscoManager: View {
@State var period: TimeInterval = 1
var body: some View {
VStack {
DiscoView(period: self.period)
HStack {
Button("Slower") {
self.period *= 2
}
Button("Faster") {
self.period /= 2
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
DiscoManager()
}
}
cukr
4
if you separate model from view, it becomes much easier to manage your state
let colors: [Color] = [.red, .green, .black, .yellow, .pink, .blue]
class DiscoManager: ObservableObject {
@Published var currentColor: Color = colors.first!
private var discoState: Int = 0
var period: TimeInterval = 1 {
didSet {
self.timer.invalidate()
self.timer = Timer.scheduledTimer(
withTimeInterval: period,
repeats: true,
block: { _ in
self.discoState = (self.discoState + 1) % colors.count
self.currentColor = colors[self.discoState]
}
)
}
}
private var timer: Timer = Timer()
init() { }
}
struct DiscoView: View {
@EnvironmentObject var discoManager: DiscoManager
var body: some View {
Text("DISCO")
.foregroundColor(self.discoManager.currentColor)
}
}
struct DiscoController: View {
let discoManager: DiscoManager = DiscoManager()
var body: some View {
VStack {
DiscoView()
HStack {
Button("Slower") {
self.discoManager.period *= 2
}
Button("Faster") {
self.discoManager.period /= 2
}
}
}.environmentObject(discoManager)
}
}
1 Like
.onReceive(
Timer.publish(every: period, on: .main, in: .default).autoconnect()
) { _ in
self.color = (self.color + 1) % colors.count
}
This creates new instance of NSTimer on every execution of body. 
Onne
(Onne van Dijk)
7
Yes, you are right, that solution was probably a bit too quick and dirty.
Update for iOS 14.
Looks like the newly introduced View.onChange(of:perform:) is an answer to this:
struct DiscoView: View {
var period: TimeInterval
@State var color: Int = 0
@State var timer: Timer? = nil
var body: some View {
Text("DISCO")
.foregroundColor(colors[color])
.onAppear {
self.timer = makeTimer()
}
.onDisappear {
self.timer?.invalidate()
}
.onChange(of: period) { [period] newPeriod in
print("Period changed from \(period) to \(newPeriod)")
if self.timer?.timeInterval != newPeriod {
self.timer?.invalidate()
self.timer = makeTimer()
}
}
}
func makeTimer() -> Timer {
Timer.scheduledTimer(withTimeInterval: self.period, repeats: true) { _ in
self.color = (self.color + 1) % colors.count
}
}
}
2 Likes