Skip to content

Commit 2208de3

Browse files
committed
swift 6 concurrency & working NSViewRepresentable.
Signed-off-by: furby™ <devs@wabi.foundation>
1 parent 499a745 commit 2208de3

20 files changed

+196
-6
lines changed
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import AppKit
2+
3+
public class BaseWidget: NSView {
4+
private var leftConstraint: NSLayoutConstraint?
5+
private var topConstraint: NSLayoutConstraint?
6+
private var widthConstraint: NSLayoutConstraint?
7+
private var heightConstraint: NSLayoutConstraint?
8+
9+
var x = 0 {
10+
didSet {
11+
if x != oldValue {
12+
updateLeftConstraint()
13+
}
14+
}
15+
}
16+
17+
var y = 0 {
18+
didSet {
19+
if y != oldValue {
20+
updateTopConstraint()
21+
}
22+
}
23+
}
24+
25+
var width = 0 {
26+
didSet {
27+
if width != oldValue {
28+
updateWidthConstraint()
29+
}
30+
}
31+
}
32+
33+
var height = 0 {
34+
didSet {
35+
if height != oldValue {
36+
updateHeightConstraint()
37+
}
38+
}
39+
}
40+
41+
init() {
42+
super.init(frame: .zero)
43+
44+
self.translatesAutoresizingMaskIntoConstraints = false
45+
}
46+
47+
@available(*, unavailable)
48+
public required init?(coder: NSCoder) {
49+
fatalError("init(coder:) is not used for this view")
50+
}
51+
52+
private func updateLeftConstraint() {
53+
leftConstraint?.isActive = false
54+
guard let superview else { return }
55+
if #available(macOS 11.0, *) {
56+
leftConstraint = self.leftAnchor.constraint(
57+
equalTo: superview.safeAreaLayoutGuide.leftAnchor, constant: CGFloat(x))
58+
} else {
59+
leftConstraint = self.leftAnchor.constraint(
60+
equalTo: superview.leftAnchor, constant: CGFloat(x))
61+
}
62+
leftConstraint!.isActive = true
63+
}
64+
65+
private func updateTopConstraint() {
66+
topConstraint?.isActive = false
67+
guard let superview else { return }
68+
if #available(macOS 11.0, *) {
69+
topConstraint = self.topAnchor.constraint(
70+
equalTo: superview.safeAreaLayoutGuide.topAnchor, constant: CGFloat(x))
71+
} else {
72+
topConstraint = self.topAnchor.constraint(
73+
equalTo: superview.topAnchor, constant: CGFloat(x))
74+
}
75+
topConstraint!.isActive = true
76+
}
77+
78+
private func updateWidthConstraint() {
79+
widthConstraint?.isActive = false
80+
widthConstraint = self.widthAnchor.constraint(equalToConstant: CGFloat(width))
81+
widthConstraint!.isActive = true
82+
}
83+
84+
private func updateHeightConstraint() {
85+
heightConstraint?.isActive = false
86+
heightConstraint = self.heightAnchor.constraint(equalToConstant: CGFloat(height))
87+
heightConstraint!.isActive = true
88+
}
89+
90+
public override func viewDidMoveToSuperview() {
91+
super.viewDidMoveToSuperview()
92+
93+
updateLeftConstraint()
94+
updateTopConstraint()
95+
}
96+
}
97+
98+
class WrapperWidget<View: NSView>: BaseWidget {
99+
init(child: View) {
100+
super.init()
101+
102+
self.addSubview(child)
103+
child.translatesAutoresizingMaskIntoConstraints = false
104+
NSLayoutConstraint.activate([
105+
child.topAnchor.constraint(equalTo: self.topAnchor),
106+
child.leadingAnchor.constraint(equalTo: self.leadingAnchor),
107+
child.bottomAnchor.constraint(equalTo: self.bottomAnchor),
108+
child.trailingAnchor.constraint(equalTo: self.trailingAnchor),
109+
])
110+
}
111+
112+
override convenience init() {
113+
self.init(child: View(frame: .zero))
114+
}
115+
116+
var child: View {
117+
subviews[0] as! View
118+
}
119+
120+
override var intrinsicContentSize: CGSize {
121+
child.intrinsicContentSize
122+
}
123+
}

Sources/AppKitBackend/NSViewRepresentable.swift

+21-3
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ extension View where Self: NSViewRepresentable {
151151
context: representingWidget.context!
152152
)
153153

154+
if !dryRun {
155+
representingWidget.width = size.size.x
156+
representingWidget.height = size.size.y
157+
}
158+
154159
return ViewUpdateResult.leafView(size: size)
155160
}
156161
}
@@ -162,20 +167,26 @@ extension NSViewRepresentable where Coordinator == Void {
162167
}
163168

164169

165-
final class RepresentingWidget<Representable: NSViewRepresentable> {
170+
final class RepresentingWidget<Representable: NSViewRepresentable>: BaseWidget {
166171
var representable: Representable
167172
var context: NSViewRepresentableContext<Representable.Coordinator>?
168173

169-
@MainActor
170174
lazy var subview: Representable.NSViewType = {
171175
let view = representable.makeNSView(context: context!)
172176

177+
self.addSubview(view)
178+
173179
view.translatesAutoresizingMaskIntoConstraints = false
180+
NSLayoutConstraint.activate([
181+
view.topAnchor.constraint(equalTo: self.topAnchor),
182+
view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
183+
view.trailingAnchor.constraint(equalTo: self.trailingAnchor),
184+
view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
185+
])
174186

175187
return view
176188
}()
177189

178-
@MainActor
179190
func update(with environment: EnvironmentValues) {
180191
if context == nil {
181192
context = .init(coordinator: representable.makeCoordinator(), environment: environment)
@@ -187,5 +198,12 @@ final class RepresentingWidget<Representable: NSViewRepresentable> {
187198

188199
init(representable: Representable) {
189200
self.representable = representable
201+
super.init()
202+
}
203+
204+
deinit {
205+
if let context {
206+
Representable.dismantleNSView(subview, coordinator: context.coordinator)
207+
}
190208
}
191209
}

Sources/SwiftCrossUI/App.swift

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ extension App {
5454
}
5555

5656
/// Runs the application.
57+
@MainActor
5758
public static func main() {
5859
swiftBundlerAppMetadata = extractSwiftBundlerMetadata()
5960

Sources/SwiftCrossUI/Modifiers/OnDisappearModifier.swift

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct OnDisappearModifier<Content: View>: View {
1313
var body: TupleView1<Content>
1414
var action: () -> Void
1515

16+
@MainActor
1617
func children<Backend: AppBackend>(
1718
backend: Backend,
1819
snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
@@ -45,6 +46,7 @@ struct OnDisappearModifier<Content: View>: View {
4546
defaultAsWidget(children.wrappedChildren, backend: backend)
4647
}
4748

49+
@MainActor
4850
func update<Backend: AppBackend>(
4951
_ widget: Backend.Widget,
5052
children: OnDisappearModifierChildren,

Sources/SwiftCrossUI/Scenes/SceneGraphNode.swift

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public protocol SceneGraphNode: AnyObject {
1010
/// Creates a node from a corresponding scene. Should perform initial setup of
1111
/// any widgets required to display the scene (although ``SceneGraphNode/update(_:)``
1212
/// is guaranteed to be called immediately after initialization).
13+
@MainActor
1314
init<Backend: AppBackend>(
1415
from scene: NodeScene,
1516
backend: Backend,
@@ -22,6 +23,7 @@ public protocol SceneGraphNode: AnyObject {
2223
/// - newScene: The new recomputed scene if the update is due to being recomputed.
2324
/// - backend: The backend to use.
2425
/// - environment: The current root-level environment.
26+
@MainActor
2527
func update<Backend: AppBackend>(
2628
_ newScene: NodeScene?,
2729
backend: Backend,

Sources/SwiftCrossUI/ViewGraph/AnyViewGraphNode.swift

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class AnyViewGraphNode<NodeView: View> {
2929
private var _getBackend: () -> any AppBackend
3030

3131
/// Type-erases a view graph node.
32+
@MainActor
3233
public init<Backend: AppBackend>(_ node: ViewGraphNode<NodeView, Backend>) {
3334
self.node = node
3435
_updateWithNewView = node.update(with:proposedSize:environment:dryRun:)
@@ -47,6 +48,7 @@ public class AnyViewGraphNode<NodeView: View> {
4748
}
4849

4950
/// Creates a new view graph node and immediately type-erases it.
51+
@MainActor
5052
public convenience init<Backend: AppBackend>(
5153
for view: NodeView,
5254
backend: Backend,

Sources/SwiftCrossUI/ViewGraph/ErasedViewGraphNode.swift

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public struct ErasedViewGraphNode {
1919
public var viewType: any View.Type
2020
public var backendType: any AppBackend.Type
2121

22+
@MainActor
2223
public init<V: View, Backend: AppBackend>(
2324
for view: V,
2425
backend: Backend,
@@ -35,6 +36,7 @@ public struct ErasedViewGraphNode {
3536
)
3637
}
3738

39+
@MainActor
3840
public init<V: View, Backend: AppBackend>(
3941
wrapping node: ViewGraphNode<V, Backend>
4042
) {
@@ -68,16 +70,19 @@ public struct ErasedViewGraphNode {
6870
}
6971
}
7072

73+
@MainActor
7174
public init<V: View>(wrapping node: AnyViewGraphNode<V>) {
7275
self.init(wrapping: node, backend: node.getBackend())
7376
}
7477

78+
@MainActor
7579
private init<V: View, Backend: AppBackend>(
7680
wrapping node: AnyViewGraphNode<V>, backend: Backend
7781
) {
7882
self.init(wrapping: node.node as! ViewGraphNode<V, Backend>)
7983
}
8084

85+
@MainActor
8186
public func transform<R>(with transformer: any ErasedViewGraphNodeTransformer<R>) -> R {
8287
func helper<V: View, Backend: AppBackend>(
8388
viewType: V.Type,
@@ -92,5 +97,6 @@ public struct ErasedViewGraphNode {
9297
public protocol ErasedViewGraphNodeTransformer<Return> {
9398
associatedtype Return
9499

100+
@MainActor
95101
func transform<V: View, Backend: AppBackend>(node: ViewGraphNode<V, Backend>) -> Return
96102
}

Sources/SwiftCrossUI/ViewGraph/ViewGraph.swift

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class ViewGraph<Root: View> {
3030
private var setIncomingURLHandler: (@escaping (URL) -> Void) -> Void
3131

3232
/// Creates a view graph for a root view with a specific backend.
33+
@MainActor
3334
public init<Backend: AppBackend>(
3435
for view: Root,
3536
backend: Backend,
@@ -71,6 +72,7 @@ public class ViewGraph<Root: View> {
7172
return result
7273
}
7374

75+
@MainActor
7476
public func snapshot() -> ViewGraphSnapshotter.NodeSnapshot {
7577
ViewGraphSnapshotter.snapshot(of: rootNode)
7678
}

Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend> {
5252

5353
/// Creates a node for a given view while also creating the nodes for its children, creating
5454
/// the view's widget, and starting to observe its state for changes.
55+
@MainActor
5556
public init(
5657
for nodeView: NodeView,
5758
backend: Backend,
@@ -139,6 +140,7 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend> {
139140
/// Triggers the view to be updated as part of a bottom-up chain of updates (where either the
140141
/// current view gets updated due to a state change and has potential to trigger its parent to
141142
/// update as well, or the current view's child has propagated such an update upwards).
143+
@MainActor
142144
private func bottomUpUpdate() {
143145
// First we compute what size the view will be after the update. If it will change size,
144146
// propagate the update to this node's parent instead of updating straight away.
@@ -173,6 +175,7 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend> {
173175
}
174176
}
175177

178+
@MainActor
176179
private func updateEnvironment(_ environment: EnvironmentValues) -> EnvironmentValues {
177180
environment.with(\.onResize) { [weak self] _ in
178181
guard let self = self else { return }
@@ -185,6 +188,7 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend> {
185188
/// is provided (in the case that the parent's body got updated) then it simply replaces the
186189
/// old view while inheriting the old view's state.
187190
/// - Parameter dryRun: If `true`, only compute sizing and don't update the underlying widget.
191+
@MainActor
188192
public func update(
189193
with newView: NodeView? = nil,
190194
proposedSize: SIMD2<Int>,

Sources/SwiftCrossUI/ViewGraph/ViewGraphNodeChildren.swift

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public protocol ViewGraphNodeChildren {
88
/// engage with annoying complexity and reducing ease of backend switching.
99
var widgets: [AnyWidget] { get }
1010
/// Erased representations of all contained child nodes.
11+
@MainActor
1112
var erasedNodes: [ErasedViewGraphNode] { get }
1213
}
1314

Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public struct ViewGraphSnapshotter: ErasedViewGraphNodeTransformer {
7474
Self.snapshot(of: AnyViewGraphNode(node))
7575
}
7676

77+
@MainActor
7778
public static func snapshot<V: View>(of node: AnyViewGraphNode<V>) -> NodeSnapshot {
7879
var stateSnapshot: [String: Data] = [:]
7980
let mirror = Mirror(reflecting: node.getView())

Sources/SwiftCrossUI/Views/AnyView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class AnyViewChildren: ViewGraphNodeChildren {
120120
}
121121

122122
/// Creates the erased child node and wraps the child's widget in a single-child container.
123+
@MainActor
123124
init<Backend: AppBackend>(
124125
from view: AnyView,
125126
backend: Backend,

Sources/SwiftCrossUI/Views/EitherView.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class EitherViewChildren<A: View, B: View>: ViewGraphNodeChildren {
138138
}
139139
}
140140

141+
@MainActor
141142
var erasedNode: ErasedViewGraphNode {
142143
switch self {
143144
case let .a(node):
@@ -158,12 +159,13 @@ class EitherViewChildren<A: View, B: View>: ViewGraphNodeChildren {
158159
var widgets: [AnyWidget] {
159160
return [node.widget]
160161
}
161-
162+
162163
var erasedNodes: [ErasedViewGraphNode] {
163164
[node.erasedNode]
164165
}
165166

166167
/// Creates storage for an either view's current child (which can change at any time).
168+
@MainActor
167169
init<Backend: AppBackend>(
168170
from view: EitherView<A, B>,
169171
backend: Backend,

Sources/SwiftCrossUI/Views/ForEach.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ extension ForEach: TypeSafeView, View where Child: View {
3333
self.elements = elements
3434
self.child = child
3535
}
36-
36+
3737
func children<Backend: AppBackend>(
3838
backend: Backend,
3939
snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
@@ -199,6 +199,7 @@ class ForEachViewChildren<
199199
}
200200

201201
/// Gets a variable length view's children as view graph node children.
202+
@MainActor
202203
init<Backend: AppBackend>(
203204
from view: ForEach<Items, Child>,
204205
backend: Backend,

Sources/SwiftCrossUI/Views/HotReloadableView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class HotReloadableViewChildren: ViewGraphNodeChildren {
118118
var hasChangedChild = true
119119

120120
/// Creates the erased child node and wraps the child's widget in a single-child container.
121+
@MainActor
121122
init<Backend: AppBackend>(
122123
from view: HotReloadableView,
123124
backend: Backend,

0 commit comments

Comments
 (0)