|
| 1 | +import Foundation |
| 2 | +import SwiftUI |
| 3 | +import Combine |
| 4 | + |
| 5 | +/// See `View.onChange(of: value, perform: action)` for more information |
| 6 | +struct ChangeObserver<Base: View, Value: Equatable>: View { |
| 7 | + let base: Base |
| 8 | + let value: Value |
| 9 | + let action: (Value)->Void |
| 10 | + |
| 11 | + let model = Model() |
| 12 | + |
| 13 | + var body: some View { |
| 14 | + if model.update(value: value) { |
| 15 | + DispatchQueue.main.async { self.action(self.value) } |
| 16 | + } |
| 17 | + return base |
| 18 | + } |
| 19 | + |
| 20 | + class Model { |
| 21 | + private var savedValue: Value? |
| 22 | + func update(value: Value) -> Bool { |
| 23 | + guard value != savedValue else { return false } |
| 24 | + savedValue = value |
| 25 | + return true |
| 26 | + } |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +extension View { |
| 31 | + /// Adds a modifier for this view that fires an action when a specific value changes. |
| 32 | + /// |
| 33 | + /// You can use `onChange` to trigger a side effect as the result of a value changing, such as an Environment key or a Binding. |
| 34 | + /// |
| 35 | + /// `onChange` is called on the main thread. Avoid performing long-running tasks on the main thread. If you need to perform a long-running task in response to value changing, you should dispatch to a background queue. |
| 36 | + /// |
| 37 | + /// The new value is passed into the closure. The previous value may be captured by the closure to compare it to the new value. For example, in the following code example, PlayerView passes both the old and new values to the model. |
| 38 | + /// |
| 39 | + /// ``` |
| 40 | + /// struct PlayerView : View { |
| 41 | + /// var episode: Episode |
| 42 | + /// @State private var playState: PlayState |
| 43 | + /// |
| 44 | + /// var body: some View { |
| 45 | + /// VStack { |
| 46 | + /// Text(episode.title) |
| 47 | + /// Text(episode.showTitle) |
| 48 | + /// PlayButton(playState: $playState) |
| 49 | + /// } |
| 50 | + /// } |
| 51 | + /// .onChange(of: playState) { [playState] newState in |
| 52 | + /// model.playStateDidChange(from: playState, to: newState) |
| 53 | + /// } |
| 54 | + /// } |
| 55 | + /// ``` |
| 56 | + /// |
| 57 | + /// - Parameters: |
| 58 | + /// - value: The value to check against when determining whether to run the closure. |
| 59 | + /// - action: A closure to run when the value changes. |
| 60 | + /// - newValue: The new value that failed the comparison check. |
| 61 | + /// - Returns: A modified version of this view |
| 62 | + func onChangeShimmed<Value: Equatable>( |
| 63 | + of value: Value, |
| 64 | + perform action: @escaping (_ newValue: Value)->Void) -> some View { |
| 65 | + if #available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) { |
| 66 | + return AnyView(self.onChange(of: value, perform: action)) |
| 67 | + } |
| 68 | + return AnyView(ChangeObserver(base: self, value: value, action: action)) |
| 69 | + } |
| 70 | +} |
0 commit comments