diff --git a/Package.swift b/Package.swift index a6b9c079..81819487 100644 --- a/Package.swift +++ b/Package.swift @@ -22,5 +22,11 @@ let package = Package( .testTarget( name: "SwiftTUITests", dependencies: ["SwiftTUI"]), + .executableTarget( + name: "SwiftTUIExample", + dependencies: [ + "SwiftTUI" + ] + ) ] ) diff --git a/Sources/SwiftTUI/PropertyWrappers/View+ObservableObject.swift b/Sources/SwiftTUI/PropertyWrappers/View+ObservableObject.swift index 74e886df..d118e5d9 100644 --- a/Sources/SwiftTUI/PropertyWrappers/View+ObservableObject.swift +++ b/Sources/SwiftTUI/PropertyWrappers/View+ObservableObject.swift @@ -4,9 +4,13 @@ import Combine extension View { func setupObservedObjectProperties(node: Node) { + log("Inside setupObservedObjectProperties") + for (label, value) in Mirror(reflecting: self).children { if let label, let observedObject = value as? AnyObservedObject { + log("subscribing to label: \(label)") node.subscriptions[label] = observedObject.subscribe { + log("ObservableObject observed a change. invalidating node...") node.root.application?.invalidateNode(node) } } diff --git a/Sources/SwiftTUI/PropertyWrappers/View+Observation.swift b/Sources/SwiftTUI/PropertyWrappers/View+Observation.swift new file mode 100644 index 00000000..56ff9100 --- /dev/null +++ b/Sources/SwiftTUI/PropertyWrappers/View+Observation.swift @@ -0,0 +1,28 @@ +import Observation +import Foundation + +@available(macOS 14.0, *) +extension View { + func setupObservableClassProperties(node: Node) { + for (_, value) in Mirror(reflecting: self).children { + if let observable = value as? Observation.Observable { + startObservation(node: node) + } + } + } + + func startObservation(node: Node) { + log("Starting observation") + withObservationTracking { + self.body + } onChange: { + defer { + Task { @MainActor in + startObservation(node: node) + } + } + log("Observation observed a change. invalidating node...") + node.root.application?.invalidateNode(node) + } + } +} diff --git a/Sources/SwiftTUI/ViewGraph/ComposedView.swift b/Sources/SwiftTUI/ViewGraph/ComposedView.swift index 3e05da21..7bcc6b57 100644 --- a/Sources/SwiftTUI/ViewGraph/ComposedView.swift +++ b/Sources/SwiftTUI/ViewGraph/ComposedView.swift @@ -10,10 +10,14 @@ struct ComposedView: GenericView { #if os(macOS) view.setupObservedObjectProperties(node: node) #endif + if #available(macOS 14.0, *) { + view.setupObservableClassProperties(node: node) + } node.addNode(at: 0, Node(view: view.body.view)) } func updateNode(_ node: Node) { + log("calling updateNode") view.setupStateProperties(node: node) view.setupEnvironmentProperties(node: node) node.view = self diff --git a/Sources/SwiftTUIExample/ExampleApp.swift b/Sources/SwiftTUIExample/ExampleApp.swift new file mode 100644 index 00000000..6e825275 --- /dev/null +++ b/Sources/SwiftTUIExample/ExampleApp.swift @@ -0,0 +1,115 @@ + +import SwiftTUI + +@main struct ExampleApp { + static func main() { + if #available(macOS 14.0, *) { + Application( + rootView: ExampleView(), + runLoopType: .cocoa + ) + .start() + } else { + // Fallback on earlier versions + log("This ExampleApp requires macOS 14.0 or newer") + } + } +} + +@available(macOS 14.0, *) +struct ExampleView: View { + var body: some View { + HStack { + ObservationView().border() + CombineView().border() + } + } +} + +// MARK: Observation example +import Observation + +@available(macOS 14.0, *) +struct ObservationView: View { + let vm: CounterViewModel + init(vm: CounterViewModel = .init()) { + self.vm = vm + } + + var body: some View { + VStack { + Text("ObservationView").underline() + if vm.showCounter { + Text("int: \(vm.int)") + } + VStack { + Button("Show/Hide Counter") { + vm.showCounter.toggle() + } + Button("↑") { + log("Incrementing...") + vm.int += 1 + log("Counter is now: \(vm.int)") + } + Button("↓") { + log("Decrementing...") + vm.int -= 1 + log("Counter is now: \(vm.int)") + } + } + } + } +} + +@available(macOS 14.0, *) +@Observable class CounterViewModel { + var int: Int + var showCounter: Bool + + init(int: Int = 0, showCounter: Bool = true) { + self.int = int + self.showCounter = showCounter + } +} + + + +// MARK: Combine Example +struct CombineView: View { + @ObservedObject var vm: CombineViewModel = CombineViewModel() + + var body: some View { + VStack { + Text("CombineView").underline() + if vm.showCounter { + Text("int: \(vm.int)") + } + VStack { + Button("Show/Hide Counter") { + vm.showCounter.toggle() + } + Button("↑") { + log("Incrementing...") + vm.int += 1 + log("Counter is now: \(vm.int)") + } + Button("↓") { + log("Decrementing...") + vm.int -= 1 + log("Counter is now: \(vm.int)") + } + } + } + } +} + +import Combine +class CombineViewModel: ObservableObject { + @Published var int = 0 + @Published var showCounter = true + + init(int: Int = 0, showCounter: Bool = true) { + self.int = int + self.showCounter = showCounter + } +}