Skip to content

Instantly share code, notes, and snippets.

@jegnux
Last active February 1, 2024 14:51
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jegnux/c3aee7957f6c372bf31a46c893a6e2a2 to your computer and use it in GitHub Desktop.
Save jegnux/c3aee7957f6c372bf31a46c893a6e2a2 to your computer and use it in GitHub Desktop.
import SwiftUI
import Combine
public struct ChangeObserver<V: Equatable>: ViewModifier {
public init(newValue: V, action: @escaping (V) -> Void) {
self.newValue = newValue
self.newAction = action
}
private typealias Action = (V) -> Void
private let newValue: V
private let newAction: Action
@State private var state: (V, Action)?
public func body(content: Content) -> some View {
if #available(iOS 14, *) {
assertionFailure("Please don't use this ViewModifer directly and use the `onChange(of:perform:)` modifier instead.")
}
return content
.onAppear()
.onReceive(Just(newValue)) { newValue in
if let (currentValue, action) = state, newValue != currentValue {
action(newValue)
}
state = (newValue, newAction)
}
}
}
extension View {
@_disfavoredOverload
@ViewBuilder public func onChange<V>(of value: V, perform action: @escaping (V) -> Void) -> some View where V: Equatable {
if #available(iOS 14, *) {
onChange(of: value, perform: action)
} else {
modifier(ChangeObserver(newValue: value, action: action))
}
}
}
@shaps80
Copy link

shaps80 commented Feb 14, 2022

Interesting, I'll definitely re-test then as I'm 99% (was 100% hahaha) certain this didn't behave this way for me. I'd be super happy to be wrong here mind you 👍

@shaps80
Copy link

shaps80 commented Feb 14, 2022

Ok I can see my issue. I thought it was a shadowed property but actually that wasn't it.

Essentially I've also ported the .task(id:) modifier on top of this. However it was there that the task id change was triggering correctly, however the value appeared to always be the previous value.

I've now identified its because in my TaskModifier implementation, I had passed my id in as a @State property which was a mistake. Removing that property wrapper/annotation now fixed my issue.

So the onChange implementation (both yours and mine) were not at all at fault. My mistake 👍

@KoCMoHaBTa
Copy link

KoCMoHaBTa commented Mar 10, 2022

What is the purpose of the onAppear() on line 22?

@KoCMoHaBTa
Copy link

Also i'm getting [SwiftUI] Modifying state during view update, this will cause undefined behavior. on line 27, which is fixable by adding .receive(on: DispatchQueue.main) operator to the Just on line 13

-> .onReceive(Just(newValue).receive(on: DispatchQueue.main)) { newValue in

@kuanfajardo
Copy link

+1 To what is the purpose of onAppear

@jegnux
Copy link
Author

jegnux commented Apr 25, 2022

@KoCMoHaBTa @kuanfajardo I don't remember well but it was probably a workaround to some bug.

@heecheon92
Copy link

This is such a beautiful solution...... wow......

@subtranix
Copy link

Great solution! Saved me. Just one question: What's the idea of putting action into state? Seems, the action never changes within the struct lifecycle

@shaps80
Copy link

shaps80 commented Nov 10, 2023

@subtranix my backports library actually has some improvements and is far more battle tested against a lot more projects id suggest checking that out.

https://github.com/shaps80/SwiftUIBackports/blob/main/Sources/SwiftUIBackports/Shared/OnChange/OnChange.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment