Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active January 23, 2024 07:53
Show Gist options
  • Star 52 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save IanKeen/a85e4ed74a10a25341c44a98f43cf386 to your computer and use it in GitHub Desktop.
Save IanKeen/a85e4ed74a10a25341c44a98f43cf386 to your computer and use it in GitHub Desktop.
PropertyWrapper: @transaction binding for SwiftUI to make changes to data supporting commit/rollback
struct User: Equatable {
var firstName: String
var lastName: String
}
@main
struct MyApp: App {
@State var value = User(firstName: "", lastName: "")
@State var showEdit = false
var body: some Scene {
WindowGroup {
VStack {
Text("First Name: \(value.firstName)")
Text("Last Name: \(value.lastName)")
Button("Edit") { showEdit = true }
}
.sheet(isPresented: $showEdit) {
UserEditView(value: $value.transaction())
}
}
}
}
struct UserEditView: View {
@Transaction var value: User
var body: some View {
VStack {
TextField("First Name", text: $value.firstName)
TextField("Last Name", text: $value.lastName)
Divider()
Button("Commit", action: $value.commit).disabled(!$value.hasChanges)
Button("Rollback", action: $value.rollback).disabled(!$value.hasChanges)
}
}
}
@main
struct MyApp: App {
@State var value = 0
var body: some Scene {
WindowGroup {
MyView(value: $value.transaction())
}
}
}
struct MyView: View {
@Transaction var value: Int
var body: some View {
VStack {
Text("Value: \(value)")
Text("HasChanges: \($value.hasChanges ? "yes" : "no")")
Divider()
Button("Increase") { value += 1 }
Button("Commit", action: $value.commit)
Button("Rollback", action: $value.rollback)
}
}
}
import SwiftUI
import Combine
@propertyWrapper
@dynamicMemberLookup
public struct Transaction<Value>: DynamicProperty {
@State private var derived: Value
@Binding private var source: Value
fileprivate init(source: Binding<Value>) {
self._source = source
self._derived = State(wrappedValue: source.wrappedValue)
}
public var wrappedValue: Value {
get { derived }
nonmutating set { derived = newValue }
}
public var projectedValue: Transaction<Value> { self }
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> Binding<T> {
return $derived[dynamicMember: keyPath]
}
public var binding: Binding<Value> { $derived }
public func commit() {
source = derived
}
public func rollback() {
derived = source
}
}
extension Transaction where Value: Equatable {
public var hasChanges: Bool { return source != derived }
}
extension Binding {
public func transaction() -> Transaction<Value> { .init(source: self) }
}
@agisilaos
Copy link

Hello @IanKeen, how that would work in a preview?

struct ChangeMindfulMinutesGoal_Previews: PreviewProvider {
    static var previews: some View {
        ChangeMindfulMinutesGoal(goal: 20)
    }
}

And I get this error:

Cannot convert value of type 'Int' to expected argument type 'Transaction<Int>'

@IanKeen
Copy link
Author

IanKeen commented Nov 18, 2020

hey @agisilaos , currently this requires you base the transaction off a Binding so you should be able to do something like:

ChangeMindfulMinutesGoal(goal: Binding.constant(20).transaction())

if you want to provide just a value you could do something like

extension Transaction {
    public init(source: Value) {
        var source = source
        let binding = Binding(get: { source }, set: { source = $0 })
        self.init(source: binding)
    }
}

then you can do

ChangeMindfulMinutesGoal(goal: .init(source: 20))

but this would be a detached object, commits don't propagate anywhere (which is fine for previews)

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