Created
May 21, 2024 17:58
-
-
Save IanKeen/59cf89ddad5462546ddb08eb0700fdd7 to your computer and use it in GitHub Desktop.
PropertyWrapper: AsyncPublished
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The view is unchanged from vanilla SwiftUI. | |
By swapping @Published to @AsyncPublished and attaching an async task to it the work and potential rollback is handled | |
*/ | |
class Model: ObservableObject { | |
@AsyncPublished var isOn: Bool = false | |
var isWorking: Bool { _isOn.isWorking } | |
init() { | |
_isOn.action = toggle | |
} | |
private func toggle(_ newValue: Bool) async throws { | |
let success = Bool.random() | |
try await Task.sleep(for: .seconds(1)) | |
if !success { | |
struct AnyError: Error { } | |
throw AnyError() | |
} | |
} | |
} | |
struct Test: View { | |
@StateObject var model = Model() | |
var body: some View { | |
HStack { | |
Toggle( | |
isOn: $model.isOn, | |
label: { | |
HStack { | |
Text("Toggle") | |
.frame(maxWidth: .infinity, alignment: .leading) | |
ProgressView() | |
.opacity(model.isWorking ? 1 : 0) | |
} | |
} | |
) | |
.allowsHitTesting(!model.isWorking) | |
} | |
.padding() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@propertyWrapper @MainActor public struct AsyncPublished<Value> { | |
public typealias AsyncAction = (Value) async throws -> Void | |
private class State { | |
var wrappedValue: Value | |
var isWorking: Bool = false | |
var current: Task<Void, Never>? | |
var action: AsyncAction = { _ in fatalError("Please configure action in parent object") } | |
init(wrappedValue: Value) { | |
self.wrappedValue = wrappedValue | |
} | |
} | |
private let state: State | |
public var action: AsyncAction { | |
get { fatalError() } | |
set { state.action = newValue } | |
} | |
public var isWorking: Bool { state.isWorking } | |
public var wrappedValue: Value { | |
get { state.wrappedValue } | |
set { state.wrappedValue = newValue } | |
} | |
public init(wrappedValue: Value) { | |
self.state = .init(wrappedValue: wrappedValue) | |
} | |
public static subscript<Instance: ObservableObject>( | |
_enclosingInstance instance: Instance, | |
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Instance, Value>, | |
storage storageKeyPath: ReferenceWritableKeyPath<Instance, Self> | |
) -> Value { | |
get { instance[keyPath: storageKeyPath].wrappedValue } | |
set { | |
func publisher<T>(_ value: T) -> ObservableObjectPublisher? { | |
return (Proxy<T>() as? ObservableObjectProxy)?.extractObjectWillChange(value) | |
} | |
let state = instance[keyPath: storageKeyPath].state | |
func updateState(_ update: (State) -> Void) { | |
let objectWillChangePublisher = _openExistential(instance as Any, do: publisher) | |
objectWillChangePublisher?.send() | |
update(state) | |
} | |
let oldValue = state.wrappedValue | |
updateState { state in | |
state.isWorking = true | |
state.wrappedValue = newValue | |
state.current = .init { | |
do { | |
try await state.action(newValue) | |
updateState { state in | |
state.isWorking = false | |
} | |
} catch { | |
updateState { state in | |
state.wrappedValue = oldValue | |
state.isWorking = false | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
private protocol ObservableObjectProxy { | |
func extractObjectWillChange<T>(_ instance: T) -> ObservableObjectPublisher | |
} | |
private struct Proxy<Base> { | |
func extract<A, B, C>(_ instance: A, _ extract: (Base) -> B) -> C { | |
return extract(instance as! Base) as! C | |
} | |
} | |
private extension Proxy: ObservableObjectProxy where Base: ObservableObject, Base.ObjectWillChangePublisher == ObservableObjectPublisher { | |
func extractObjectWillChange<T>(_ instance: T) -> ObservableObjectPublisher { | |
extract(instance) { $0.objectWillChange } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment