Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Created May 21, 2024 17:58
Show Gist options
  • Save IanKeen/59cf89ddad5462546ddb08eb0700fdd7 to your computer and use it in GitHub Desktop.
Save IanKeen/59cf89ddad5462546ddb08eb0700fdd7 to your computer and use it in GitHub Desktop.
PropertyWrapper: AsyncPublished
/*
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()
}
}
@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