Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Examples/Bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ version = '0.1.0'
identifier = 'dev.swiftcrossui.ColorsExample'
product = 'ColorsExample'
version = '0.1.0'

[apps.ObservableExample]
identifier = 'dev.swiftcrossui.ObservableExample'
product = 'ObservableExample'
version = '0.1.0'
11 changes: 10 additions & 1 deletion Examples/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Examples/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ let package = Package(
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13), .visionOS(.v1)],
dependencies: [
.package(name: "swift-cross-ui", path: ".."),
.package(
url: "https://github.com/pointfreeco/swift-perception.git",
from: "2.0.10"
),
] + hotReloadingDependencies,
targets: [
.executableTarget(
Expand Down Expand Up @@ -98,6 +102,11 @@ let package = Package(
.executableTarget(
name: "ColorsExample",
dependencies: exampleDependencies
)
),
.executableTarget(
name: "ObservableExample",
dependencies: exampleDependencies
+ [.product(name: "Perception", package: "swift-perception")]
),
]
)
77 changes: 77 additions & 0 deletions Examples/Sources/ObservableExample/ObservableApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import DefaultBackend
import SwiftCrossUI

#if canImport(SwiftBundlerRuntime)
import SwiftBundlerRuntime
#endif

@main
@HotReloadable
struct ObservableApp: App {
@State var model = ObservableModel()
@State var count = 0

var body: some Scene {
WindowGroup(model.windowTitle) {
VStack(spacing: 32) {
Text(model.windowText)
HStack {
View1(model: model)
.padding()
View2(model: model)
.padding()
}
ModifyingView(model: model)
.padding()
if !model.automaticModeIsOn {
Button("Start automatic Mode") {
model.startAutomaticMode()
}
}
}
.padding()
}
.defaultSize(width: 400, height: 200)
}
}

struct View1: View {
let model: ObservableModel

var body: some View {
VStack {
Text(model.view1Text)
ModifyingView(model: model)
}
.padding()
.background(Color.green)
}
}

struct View2: View {
let model: ObservableModel

var body: some View {
VStack {
Text(model.view2Text)
ModifyingView(model: model)
}
.padding()
.background(Color.red)
}
}

struct ModifyingView: View {
@Bindable var model: ObservableModel

var body: some View {
if !model.automaticModeIsOn {
VStack {
TextField("Window Title", text: $model.windowTitle)
TextField("Window Text", text: $model.windowText)
TextField("View 1 Text", text: $model.view1Text)
TextField("View 2 Text", text: $model.view2Text)
}
}
}
}
58 changes: 58 additions & 0 deletions Examples/Sources/ObservableExample/ObservableModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation
import Perception // Also works with `Observation`

private var animalList = [
"Dog",
"Cat",
"Horse",
"Elephant",
"Giraffe",
"Zebra",
"Mouse",
"Bird",
"Fish",
"Lizard",
"Turtle",
"Octopus",
"Snake",
"Crab",
"Ant",
"Bee",
"Butterfly",
"Bat",
"Bat-eared fox",
"Owl",
]

@Perceptible // Also works with `@Observable`
class ObservableModel {
var automaticModeIsOn = false
var windowTitle: String = "Window Title"
var windowText: String = "Window Text"
var view1Text: String = "View 1 Text"
var view2Text: String = "View 2 Text"

func startAutomaticMode() {
guard !automaticModeIsOn else { return }
automaticModeIsOn = true
Task {
while true {
// Wait one second before changing the next text
try await Task.sleep(nanoseconds: 1_000_000_000)

let animal = animalList.randomElement()!
let textIndex = Int.random(in: 0..<4)
switch textIndex {
case 0:
windowTitle = animal
case 1:
windowText = animal
case 2:
view1Text = animal
default:
view2Text = animal
}
}
}
}
}
20 changes: 19 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ let package = Package(
url: "https://github.com/swhitty/swift-mutex",
.upToNextMinor(from: "0.0.6")
),
.package(
url: "https://github.com/pointfreeco/swift-perception.git",
from: "2.0.10"
),
// .package(
// url: "https://github.com/stackotter/TermKit",
// revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704"
Expand All @@ -181,6 +185,7 @@ let package = Package(
.product(name: "ImageFormats", package: "swift-image-formats"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Mutex", package: "swift-mutex"),
.product(name: "PerceptionCore", package: "swift-perception"),
],
exclude: [
"Builders/ViewBuilder.swift.gyb",
Expand Down
17 changes: 14 additions & 3 deletions Sources/SwiftCrossUI/Scenes/WindowReference.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation
import PerceptionCore

/// Holds the view graph and window handle for a single window.
@MainActor
final class WindowReference<SceneType: WindowingScene> {
final class WindowReference<SceneType: WindowingScene>: ViewModelObserver {
/// The scene.
private var scene: SceneType
/// The view graph of the window's root view.
Expand All @@ -15,6 +18,9 @@ final class WindowReference<SceneType: WindowingScene> {
private let containerWidget: AnyWidget
/// The window's preferred color scheme, cached from the last update.
private var preferredColorScheme: ColorScheme?

/// Used by the `ViewModelObserver` protocol to prevent duplicate view updates.
var currentViewModelObservationID: UUID?

/// - Parameters:
/// - closeHandler: The action to perform when the window is closed. Should
Expand Down Expand Up @@ -173,9 +179,10 @@ final class WindowReference<SceneType: WindowingScene> {
if let preferredColorScheme {
environment.colorScheme = preferredColorScheme
}


let content = self.observe(in: backend) { newScene?.content() }
let probingResult = viewGraph.computeLayout(
with: newScene?.content(),
with: content,
proposedSize: .zero,
environment: environment
.with(\.allowLayoutCaching, true)
Expand Down Expand Up @@ -285,6 +292,10 @@ final class WindowReference<SceneType: WindowingScene> {
isFirstUpdate = false
}
}

func viewModelDidChange<Backend: AppBackend>(backend: Backend) {
self.update(self.scene, backend: backend, environment: self.parentEnvironment)
}

func activate<Backend: AppBackend>(backend: Backend) {
guard let window = window as? Backend.Window else {
Expand Down
Loading