@webspatial/platform-visionos 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/package.json +2 -2
  2. package/web-spatial/EventEmitter.swift +56 -0
  3. package/web-spatial/JSBCommand.swift +348 -0
  4. package/web-spatial/SpatialObject.swift +108 -0
  5. package/web-spatial/WebMsgCommand.swift +137 -0
  6. package/web-spatial/WebSpatialApp.swift +111 -0
  7. package/web-spatial/{libs/uiKitDelegate/Window.swift → Window.swift} +1 -0
  8. package/web-spatial/manager/Dynamic3DManager.swift +114 -0
  9. package/web-spatial/manager/JSBManager.swift +148 -0
  10. package/web-spatial/manager/WKWebViewManager.swift +39 -0
  11. package/web-spatial/{libs/webView/manifest.swift → manifest.swift} +16 -5
  12. package/web-spatial/model/SpatialApp.swift +190 -0
  13. package/web-spatial/model/SpatialScene.swift +1078 -0
  14. package/web-spatial/model/Spatialized2DElement.swift +128 -0
  15. package/web-spatial/model/SpatializedDynamic3DElement.swift +34 -0
  16. package/web-spatial/model/SpatializedElement.swift +95 -0
  17. package/web-spatial/model/SpatializedStatic3DElement.swift +19 -0
  18. package/web-spatial/model/dynamic3d/Geometry.swift +95 -0
  19. package/web-spatial/model/dynamic3d/SpatialComponent.swift +72 -0
  20. package/web-spatial/model/dynamic3d/SpatialEntity.swift +211 -0
  21. package/web-spatial/model/dynamic3d/SpatialMaterial.swift +39 -0
  22. package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +39 -0
  23. package/web-spatial/model/dynamic3d/SpatialModelResource.swift +38 -0
  24. package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +18 -0
  25. package/web-spatial/protocol/ScrollAbleSpatialElementContainer.swift +1 -0
  26. package/web-spatial/protocol/SpatialScrollAble.swift +8 -0
  27. package/web-spatial/protocol/SpatializedElementContainer.swift +8 -0
  28. package/web-spatial/protocol/WebMsgSender.swift +5 -0
  29. package/web-spatial/types/Vec2.swift +12 -0
  30. package/web-spatial/types/Vec3.swift +7 -0
  31. package/web-spatial/view/SceneHandlerUIView.swift +92 -0
  32. package/web-spatial/{views/ui/NavView.swift → view/SpatialNavView.swift} +149 -77
  33. package/web-spatial/view/SpatialSceneContentView.swift +218 -0
  34. package/web-spatial/view/SpatialSceneView.swift +53 -0
  35. package/web-spatial/view/Spatialized2DElementView.swift +96 -0
  36. package/web-spatial/view/SpatializedDynamic3DView.swift +173 -0
  37. package/web-spatial/view/SpatializedElementView.swift +178 -0
  38. package/web-spatial/view/SpatializedStatic3DView.swift +72 -0
  39. package/web-spatial/{views → view/view-modifier}/MaterialWithBorderCornerModifier.swift +28 -8
  40. package/web-spatial/webview/SpatialWebController.swift +300 -0
  41. package/web-spatial/webview/SpatialWebView.swift +34 -0
  42. package/web-spatial/webview/SpatialWebViewModel.swift +307 -0
  43. package/web-spatial.xcodeproj/project.pbxproj +126 -207
  44. package/web-spatial/libs/EventEmitter.swift +0 -25
  45. package/web-spatial/libs/SpatialComponent.swift +0 -24
  46. package/web-spatial/libs/SpatialEntity.swift +0 -172
  47. package/web-spatial/libs/SpatialInputComponent.swift +0 -19
  48. package/web-spatial/libs/SpatialMeshResource.swift +0 -12
  49. package/web-spatial/libs/SpatialModel3DComponent.swift +0 -51
  50. package/web-spatial/libs/SpatialModelComponent.swift +0 -25
  51. package/web-spatial/libs/SpatialObject.swift +0 -140
  52. package/web-spatial/libs/SpatialPhysicallyBasedMaterial.swift +0 -19
  53. package/web-spatial/libs/SpatialViewComponent.swift +0 -8
  54. package/web-spatial/libs/SpatialWindowComponent.swift +0 -454
  55. package/web-spatial/libs/SpatialWindowContainer.swift +0 -153
  56. package/web-spatial/libs/Utils/CommandManager.swift +0 -823
  57. package/web-spatial/libs/Utils/PerfClock.swift +0 -43
  58. package/web-spatial/libs/Utils/SceneManager.swift +0 -101
  59. package/web-spatial/libs/Utils/WindowContainerMgr.swift +0 -122
  60. package/web-spatial/libs/json/JsonParser.swift +0 -45
  61. package/web-spatial/libs/webView/UpdateSystem.swift +0 -26
  62. package/web-spatial/libs/webView/backend/NativeWebView.swift +0 -350
  63. package/web-spatial/views/ImmersiveView.swift +0 -17
  64. package/web-spatial/views/OpenDismissHandlerUI.swift +0 -45
  65. package/web-spatial/views/PlainWindowContainerView.swift +0 -132
  66. package/web-spatial/views/SpatialModel3DView.swift +0 -187
  67. package/web-spatial/views/SpatialViewUI.swift +0 -168
  68. package/web-spatial/views/SpatialWebViewUI.swift +0 -179
  69. package/web-spatial/views/VolumetricWindowContainerView.swift +0 -30
  70. package/web-spatial/web_spatialApp.swift +0 -141
  71. /package/web-spatial/{libs/Utils → Utils}/ColorExtension.swift +0 -0
  72. /package/web-spatial/{libs/Utils → Utils}/Logger.swift +0 -0
  73. /package/web-spatial/{views → view}/LoadingView.swift +0 -0
  74. /package/web-spatial/{views → view/view-modifier}/HideViewModifier.swift +0 -0
@@ -0,0 +1,178 @@
1
+ import SwiftUI
2
+
3
+ // zIndex() have some bug, so use zOrderBias to simulate zIndex effect
4
+ let zOrderBias = 0.001
5
+
6
+ struct SpatializedElementView<Content: View>: View {
7
+ @Environment(SpatializedElement.self) var spatializedElement: SpatializedElement
8
+ @Environment(SpatialScene.self) var spatialScene: SpatialScene
9
+
10
+ var parentScrollOffset: Vec2
11
+ var content: Content
12
+
13
+ init(parentScrollOffset: Vec2, @ViewBuilder content: () -> Content) {
14
+ self.parentScrollOffset = parentScrollOffset
15
+ self.content = content()
16
+ }
17
+
18
+ // Begin Interaction
19
+ var gesture: some Gesture {
20
+ DragGesture()
21
+ .onChanged(onDragging)
22
+ .onEnded(onDraggingEnded)
23
+ .simultaneously(with:
24
+ RotateGesture3D()
25
+ .onChanged(onRotateGesture3D)
26
+ .onEnded(onRotateGesture3DEnd)
27
+ )
28
+ .simultaneously(with:
29
+ MagnifyGesture()
30
+ .onChanged(onMagnifyGesture)
31
+ .onEnded(onMagnifyGestureEnd)
32
+ )
33
+ .simultaneously(with:
34
+ SpatialTapGesture(count: 1)
35
+ .onEnded(onTapEnded)
36
+ )
37
+ }
38
+
39
+ private func onRotateGesture3D(_ event: RotateGesture3D.Value) {
40
+ if spatializedElement.enableRotateGesture || spatializedElement.enableRotateStartGesture {
41
+ let gestureEvent = WebSpatialRotateGuestureEvent(
42
+ detail: .init(
43
+ rotation: event.rotation,
44
+ startAnchor3D: event.startAnchor3D,
45
+ startLocation3D: event.startLocation3D
46
+ ))
47
+ spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
48
+ }
49
+ }
50
+
51
+ private func onRotateGesture3DEnd(_ event: RotateGesture3D.Value) {
52
+ if spatializedElement.enableRotateEndGesture {
53
+ let gestureEvent = WebSpatialRotateEndGuestureEvent(
54
+ detail: .init(
55
+ rotation: event.rotation,
56
+ startAnchor3D: event.startAnchor3D,
57
+ startLocation3D: event.startLocation3D
58
+ ))
59
+ spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
60
+ }
61
+ }
62
+
63
+ private func onDragging(_ event: DragGesture.Value) {
64
+ if spatializedElement.enableDragStartGesture || spatializedElement.enableDragGesture {
65
+ let gestureEvent = WebSpatialDragGuestureEvent(detail: .init(
66
+ location3D: event.location3D,
67
+ startLocation3D: event.startLocation3D,
68
+ translation3D: event.translation3D,
69
+ predictedEndTranslation3D: event.predictedEndTranslation3D,
70
+ predictedEndLocation3D: event.predictedEndLocation3D,
71
+ velocity: event.velocity
72
+ ))
73
+ spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
74
+ }
75
+ }
76
+
77
+ private func onDraggingEnded(_ event: DragGesture.Value) {
78
+ if spatializedElement.enableDragEndGesture {
79
+ let gestureEvent = WebSpatialDragEndGuestureEvent(
80
+ detail: .init(
81
+ location3D: event.location3D,
82
+ startLocation3D: event.startLocation3D,
83
+ translation3D: event.translation3D,
84
+ predictedEndTranslation3D: event.predictedEndTranslation3D,
85
+ predictedEndLocation3D: event.predictedEndLocation3D,
86
+ velocity: event.velocity
87
+ ))
88
+ spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
89
+ }
90
+ }
91
+
92
+ private func onTapEnded(_ event: SpatialTapGesture.Value) {
93
+ if spatializedElement.enableTapGesture {
94
+ spatialScene.sendWebMsg(spatializedElement.id, WebSpatialTapGuestureEvent(detail: .init(location3D: event.location3D)))
95
+ }
96
+ }
97
+
98
+ private func onMagnifyGesture(_ event: MagnifyGesture.Value) {
99
+ if spatializedElement.enableMagnifyGesture || spatializedElement.enableMagnifyStartGesture {
100
+ let gestureEvent = WebSpatialMagnifyGuestureEvent(
101
+ detail: .init(
102
+ magnification: event.magnification,
103
+ velocity: event.velocity,
104
+ startLocation3D: event.startLocation3D,
105
+ startAnchor3D: event.startAnchor3D
106
+ ))
107
+ spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
108
+ }
109
+ }
110
+
111
+ private func onMagnifyGestureEnd(_ event: MagnifyGesture.Value) {
112
+ if spatializedElement.enableMagnifyEndGesture {
113
+ let gestureEvent = WebSpatialMagnifyEndGuestureEvent(
114
+ detail: .init(
115
+ magnification: event.magnification,
116
+ velocity: event.velocity,
117
+ startLocation3D: event.startLocation3D,
118
+ startAnchor3D: event.startAnchor3D
119
+ ))
120
+ spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
121
+ }
122
+ }
123
+
124
+ // End Interaction
125
+
126
+ @ViewBuilder
127
+ var body: some View {
128
+ let transform = spatializedElement.transform
129
+ let translation = transform.translation
130
+ let scale = transform.scale
131
+ let rotation = transform.rotation!
132
+
133
+ let width = spatializedElement.width
134
+ let height = spatializedElement.height
135
+ let depth = spatializedElement.depth
136
+ let anchor = spatializedElement.rotationAnchor
137
+
138
+ let centerX = spatializedElement.clientX - (spatializedElement.scrollWithParent ? parentScrollOffset.x : 0)
139
+ let centerY = spatializedElement.clientY - (spatializedElement.scrollWithParent ? parentScrollOffset.y : 0)
140
+
141
+ let opacity = spatializedElement.opacity
142
+ let visible = spatializedElement.visible
143
+ let enableGesture = spatializedElement.enableGesture
144
+
145
+ let z = translation.z + (spatializedElement.zIndex * zOrderBias)
146
+ let smallOffset = z == 0.0 ? 0.0001 : 0
147
+
148
+ content.simultaneousGesture(enableGesture ? gesture : nil)
149
+ .frame(width: width, height: height)
150
+ .frame(depth: depth, alignment: .back)
151
+ .onGeometryChange3D(for: AffineTransform3D.self) { proxy in
152
+ let rect3d = proxy.frame(in: .named("SpatialScene"))
153
+ spatialScene.sendWebMsg(spatializedElement.id, SpatiaizedContainerClientCube(origin: rect3d.origin, size: rect3d.size))
154
+ return proxy.transform(in: .named("SpatialScene"))!
155
+ } action: { _, new in
156
+ spatialScene.sendWebMsg(spatializedElement.id, SpatiaizedContainerTransform(detail: new))
157
+ }
158
+ .frame(depth: 0, alignment: .back)
159
+ // use .offset(smallVal) to workaround for glassEffect not working and small width/height spatialDiv not working
160
+ .offset(z: smallOffset)
161
+ .scaleEffect(
162
+ x: scale.width,
163
+ y: scale.height,
164
+ z: scale.depth,
165
+ anchor: anchor
166
+ )
167
+ .rotation3DEffect(
168
+ rotation,
169
+ anchor: anchor
170
+ )
171
+ .offset(x: translation.x, y: translation.y)
172
+ .offset(z: z)
173
+ .position(x: centerX + width / 2, y: centerY + height / 2)
174
+ .offset(z: spatializedElement.backOffset)
175
+ .opacity(opacity)
176
+ .hidden(!visible)
177
+ }
178
+ }
@@ -0,0 +1,72 @@
1
+ import RealityKit
2
+ import SwiftUI
3
+
4
+ struct SpatializedStatic3DView: View {
5
+ @Environment(SpatializedElement.self) var spatializedElement: SpatializedElement
6
+ @Environment(SpatialScene.self) var spatialScene: SpatialScene
7
+
8
+ private var spatializedStatic3DElement: SpatializedStatic3DElement {
9
+ return spatializedElement as! SpatializedStatic3DElement
10
+ }
11
+
12
+ func onLoadSuccess() {
13
+ spatialScene.sendWebMsg(spatializedElement.id, ModelLoadSuccess())
14
+ }
15
+
16
+ func onLoadFailure() {
17
+ spatialScene.sendWebMsg(spatializedElement.id, ModelLoadFailure())
18
+ }
19
+
20
+ @ViewBuilder
21
+ var body: some View {
22
+ let depth = spatializedElement.depth
23
+ let transform = spatializedStatic3DElement.modelTransform
24
+ let translation = transform.translation
25
+ let scale = transform.scale
26
+ let rotation = transform.rotation!
27
+ let x = translation.x
28
+ let y = translation.y
29
+ let z = translation.z
30
+
31
+ let enableGesture = spatializedElement.enableGesture
32
+ if let url = URL(string: spatializedStatic3DElement.modelURL) {
33
+ Model3D(url: url) { newPhase in
34
+ switch newPhase {
35
+ case .empty:
36
+ ProgressView()
37
+
38
+ case let .success(resolvedModel3D):
39
+ resolvedModel3D
40
+ .resizable(true)
41
+ .aspectRatio(
42
+ nil,
43
+ contentMode: .fit
44
+ )
45
+ .if(!depth.isZero){ view in view.scaledToFit3D()}
46
+ .onAppear {
47
+ self.onLoadSuccess()
48
+ }
49
+ .if(enableGesture) { view in view.hoverEffect()}
50
+ case .failure:
51
+ Text("").onAppear {
52
+ self.onLoadFailure()
53
+ }
54
+ @unknown default:
55
+ EmptyView()
56
+ }
57
+ }
58
+ .scaleEffect(
59
+ x: scale.width,
60
+ y: scale.height,
61
+ z: scale.depth
62
+ )
63
+ .rotation3DEffect(
64
+ rotation
65
+ )
66
+ .offset(x: x, y: y)
67
+ .offset(z: z)
68
+ } else {
69
+ EmptyView()
70
+ }
71
+ }
72
+ }
@@ -49,10 +49,17 @@ struct CornerRadius: Codable {
49
49
  struct MaterialWithBorderCornerModifier: ViewModifier {
50
50
  let backgroundMaterial: BackgroundMaterial
51
51
  let cornerRadius: CornerRadius
52
+ let windowStyle: SpatialScene.WindowStyle
52
53
 
53
- init(_ backgroundMaterial: BackgroundMaterial, _ cornerRadius: CornerRadius) {
54
+ init(
55
+ _ backgroundMaterial: BackgroundMaterial,
56
+ _ cornerRadius: CornerRadius,
57
+ _ windowStyle: SpatialScene
58
+ .WindowStyle
59
+ ) {
54
60
  self.backgroundMaterial = backgroundMaterial
55
61
  self.cornerRadius = cornerRadius
62
+ self.windowStyle = windowStyle
56
63
  }
57
64
 
58
65
  func body(content: Content) -> some View {
@@ -60,11 +67,20 @@ struct MaterialWithBorderCornerModifier: ViewModifier {
60
67
 
61
68
  switch backgroundMaterial {
62
69
  case .GlassMaterial:
63
- content
64
- .glassBackgroundEffect(
65
- in: .rect(cornerRadii: radii),
66
- displayMode: .always
67
- )
70
+ if windowStyle == .volume {
71
+ content
72
+ .glassBackgroundEffect(
73
+ in: .rect(cornerRadii: radii),
74
+ displayMode: .always
75
+ )
76
+ } else {
77
+ content
78
+ .glassBackgroundEffect(
79
+ in: .rect(cornerRadii: radii),
80
+ displayMode: .always
81
+ )
82
+ .frame(depth: 0)
83
+ }
68
84
 
69
85
  case .RegularMaterial:
70
86
  content
@@ -89,9 +105,13 @@ struct MaterialWithBorderCornerModifier: ViewModifier {
89
105
  }
90
106
 
91
107
  extension View {
92
- func materialWithBorderCorner(_ backgroundMaterial: BackgroundMaterial, _ cornerRadius: CornerRadius) -> some View {
108
+ func materialWithBorderCorner(_ backgroundMaterial: BackgroundMaterial, _ cornerRadius: CornerRadius, _ windowStyle: SpatialScene.WindowStyle) -> some View {
93
109
  return modifier(
94
- MaterialWithBorderCornerModifier(backgroundMaterial, cornerRadius)
110
+ MaterialWithBorderCornerModifier(
111
+ backgroundMaterial,
112
+ cornerRadius,
113
+ windowStyle
114
+ )
95
115
  )
96
116
  }
97
117
  }
@@ -0,0 +1,300 @@
1
+ import SwiftUI
2
+ @preconcurrency import WebKit
3
+
4
+ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandlerWithReply, WKUIDelegate, UIScrollViewDelegate, WKURLSchemeHandler {
5
+ weak var model: SpatialWebViewModel?
6
+ var webview: WKWebView?
7
+ private var isObserving = false
8
+ private var navigationInvoke: ((_ data: URL) -> Bool)?
9
+ private var openWindowInvoke: ((_ data: URL) -> WebViewElementInfo?)?
10
+ private var webviewStateChangeInvoke: ((_ type: SpatialWebViewState) -> Void)?
11
+ private var scorllUpdateInvoke: ((_ type: ScrollState, _ point: CGPoint) -> Void)?
12
+ private var webviewTitle: String? = nil
13
+ private var firstLoad = true
14
+ private var jsbManager = JSBManager()
15
+
16
+ override init() {
17
+ WKWebView.enableFileScheme() // ensure the handler is usable
18
+ }
19
+
20
+ deinit {}
21
+
22
+ func registerNavigationInvoke(invoke: @escaping (_ data: URL) -> Bool) {
23
+ navigationInvoke = invoke
24
+ }
25
+
26
+ func registerOpenWindowInvoke(invoke: @escaping (_ data: URL) -> WebViewElementInfo?) {
27
+ openWindowInvoke = invoke
28
+ }
29
+
30
+ func registeJSBHandler<T: CommandDataProtocol>(_ type: T.Type, _ event: @escaping (T, @escaping JSBManager.ResolveHandler<Encodable>) -> Void) {
31
+ jsbManager.register(type, event)
32
+ }
33
+
34
+ func registeJSBHandler<T: CommandDataProtocol>(_ type: T.Type, _ event: @escaping (@escaping JSBManager.ResolveHandler<Encodable>) -> Void) {
35
+ jsbManager.register(type, event)
36
+ }
37
+
38
+ func unregisterJSBHandler<T: CommandDataProtocol>(_ type: T.Type) {
39
+ jsbManager.remove(type)
40
+ }
41
+
42
+ func clearJSBHandler() {
43
+ jsbManager.clear()
44
+ }
45
+
46
+ func mockJSB(_ command: String) {
47
+ jsbManager.handlerMessage(command)
48
+ }
49
+
50
+ func registerWebviewStateChangeInvoke(invoke: @escaping (_ type: SpatialWebViewState) -> Void) {
51
+ webviewStateChangeInvoke = invoke
52
+ }
53
+
54
+ func registerScrollUpdateInvoke(invoke: @escaping (_ type: ScrollState, _ point: CGPoint) -> Void) {
55
+ scorllUpdateInvoke = invoke
56
+ }
57
+
58
+ func setWebViewTitle(_ title: String) {
59
+ webviewTitle = title
60
+ if webview != nil {
61
+ callJS("document.title='\(title)'")
62
+ }
63
+ }
64
+
65
+ // navigation request
66
+ // SpatialDiv/forcestyle/normal web link protocol
67
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
68
+ let deciside = navigationInvoke?(navigationAction.request.url!)
69
+ if deciside == true {
70
+ if !firstLoad{
71
+ webviewStateChangeInvoke?(.didUnload)
72
+ }
73
+ firstLoad = false
74
+ }
75
+ var needAllow = deciside ?? false
76
+
77
+ if !needAllow{
78
+ UIApplication.shared.open(navigationAction.request.url!, options: [:], completionHandler: nil)
79
+ }
80
+ decisionHandler(needAllow ? .allow : .cancel)
81
+ }
82
+
83
+ // open window request
84
+ func webView(
85
+ _ webView: WKWebView,
86
+ createWebViewWith configuration: WKWebViewConfiguration,
87
+ for navigationAction: WKNavigationAction,
88
+ windowFeatures: WKWindowFeatures
89
+ ) -> WKWebView? {
90
+ if let modelInfo = openWindowInvoke?(navigationAction.request.url!) {
91
+ modelInfo.element.load(configuration, modelInfo.id)
92
+ return modelInfo.element.getController().webview
93
+ }
94
+ print("no webview")
95
+ return nil
96
+ }
97
+
98
+ // invoke jsb
99
+ func userContentController(
100
+ _ userContentController: WKUserContentController,
101
+ didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void
102
+ ) {
103
+ // let promise = JSBManager.Promise(replyHandler)
104
+ jsbManager.handlerMessage(message.body as! String, replyHandler)
105
+ }
106
+
107
+ // custom scheme request
108
+ func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
109
+ print("urlSchemeTask")
110
+ let url = urlSchemeTask.request.url
111
+ if url!.absoluteString.starts(with: "file://") {
112
+ let urlRequest = urlSchemeTask.request
113
+
114
+ let session = URLSession(configuration: URLSessionConfiguration.default)
115
+ let dataTask = session.dataTask(with: urlRequest) { [task = urlSchemeTask as AnyObject] data, response, _ in
116
+ guard let task = task as? WKURLSchemeTask else { return }
117
+
118
+ task.didReceive(response!)
119
+ task.didReceive(data!)
120
+ task.didFinish()
121
+ }
122
+ dataTask.resume()
123
+ }
124
+ }
125
+
126
+ func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) {}
127
+ func webView(_ webView: WKWebView, didStartProvisionalNavigation: WKNavigation!) {
128
+ webviewStateChangeInvoke?(.didStartLoad)
129
+ }
130
+
131
+ func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
132
+ webviewStateChangeInvoke?(.didReceive)
133
+ }
134
+
135
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
136
+ webviewStateChangeInvoke?(.didFinishLoad)
137
+ if webviewTitle != nil {
138
+ callJS("document.title='\(webviewTitle!)'")
139
+ }
140
+ // flush pending calljs comand
141
+ isPageLoaded = true
142
+ flushJSQueue()
143
+ }
144
+
145
+ func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Swift.Void) {
146
+ decisionHandler(.allow)
147
+ }
148
+
149
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
150
+ if let urlError = (error as? URLError) {
151
+ if urlError.code == .cannotConnectToHost {
152
+ webviewStateChangeInvoke?(.didFailLoad)
153
+ }
154
+ }
155
+ }
156
+
157
+ func webViewDidClose(_ webView: WKWebView) {
158
+ webviewStateChangeInvoke?(.didClose)
159
+ }
160
+
161
+ func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
162
+ guard let serverTrust = challenge.protectionSpace.serverTrust else { return completionHandler(.useCredential, nil) }
163
+ let exceptions = SecTrustCopyExceptions(serverTrust)
164
+ SecTrustSetExceptions(serverTrust, exceptions)
165
+ completionHandler(.useCredential, URLCredential(trust: serverTrust))
166
+ }
167
+
168
+ func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
169
+ scorllUpdateInvoke?(.start, scrollView.contentOffset)
170
+ }
171
+
172
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
173
+ scorllUpdateInvoke?(.update, scrollView.contentOffset)
174
+ }
175
+
176
+ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
177
+ scorllUpdateInvoke?(.end, scrollView.contentOffset)
178
+ }
179
+
180
+ func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
181
+ if !decelerate {
182
+ scorllUpdateInvoke?(.end, scrollView.contentOffset)
183
+ } else {
184
+ scorllUpdateInvoke?(.release, scrollView.contentOffset)
185
+ }
186
+ }
187
+
188
+ func startObserving() {
189
+ guard !isObserving else { return }
190
+ webview?.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)
191
+ isObserving = true
192
+ }
193
+
194
+ func stopObserving() {
195
+ guard isObserving else { return }
196
+ webview?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
197
+ isObserving = false
198
+ }
199
+
200
+ override func observeValue(
201
+ forKeyPath keyPath: String?,
202
+ of object: Any?,
203
+ change: [NSKeyValueChangeKey: Any]?,
204
+ context: UnsafeMutableRawPointer?
205
+ ) {
206
+ if keyPath == #keyPath(WKWebView.url),
207
+ let url = (object as? WKWebView)?.url?.absoluteString
208
+ {
209
+ DispatchQueue.main.async {
210
+ // print("url change", url)
211
+ self.model?.url = url
212
+ }
213
+ }
214
+ }
215
+
216
+ func destroy() {
217
+ destroyView()
218
+ navigationInvoke = nil
219
+ openWindowInvoke = nil
220
+ webviewStateChangeInvoke = nil
221
+ scorllUpdateInvoke = nil
222
+ model = nil
223
+ }
224
+
225
+ private var state:SpatialWebViewState?
226
+
227
+ func destroyView() {
228
+ stopObserving()
229
+ if webview != nil {
230
+ webview?.stopLoading()
231
+ webview?.configuration.userContentController.removeScriptMessageHandler(forName: "bridge")
232
+ webview?.uiDelegate = nil
233
+ webview?.navigationDelegate = nil
234
+ webview?.scrollView.delegate = nil
235
+ webview = nil
236
+ webviewStateChangeInvoke?(.didDestroyView)
237
+ }
238
+ }
239
+
240
+ private var isPageLoaded = false
241
+
242
+ private var jsQueue: [String] = []
243
+
244
+ private func enqueueJS(_ js: String) {
245
+ jsQueue.append(js)
246
+ }
247
+
248
+ private func flushJSQueue() {
249
+ guard !jsQueue.isEmpty else { return }
250
+ let combined = jsQueue.joined(separator: ";")
251
+ callJS(combined)
252
+ jsQueue.removeAll()
253
+ }
254
+
255
+ func callJS(_ js: String) {
256
+ if webview != nil && isPageLoaded {
257
+ webview!.evaluateJavaScript(js)
258
+ } else {
259
+ enqueueJS(js)
260
+ }
261
+ }
262
+ }
263
+
264
+ enum ScrollState {
265
+ case start
266
+ case update
267
+ case release
268
+ case end
269
+ }
270
+
271
+ // extend webview to support file://
272
+ @available(iOS 11.0, *)
273
+ extension WKWebView {
274
+ /// WKWebView, Support setting file scheme in configuration
275
+ public private(set) static var isEnableFileSupport = false
276
+ public static func enableFileScheme() {
277
+ /// This method supports adapting supported files through Configuration, but cannot be cancelled (Configuration is immutable).
278
+ if !isEnableFileSupport {
279
+ switchHandlesURLScheme()
280
+ }
281
+ }
282
+
283
+ private static func switchHandlesURLScheme() {
284
+ if
285
+ case let cls = WKWebView.self,
286
+ let m1 = class_getClassMethod(cls, NSSelectorFromString("handlesURLScheme:")),
287
+ let m2 = class_getClassMethod(cls, #selector(WKWebView.wrapHandles(urlScheme:)))
288
+ {
289
+ method_exchangeImplementations(m1, m2)
290
+ isEnableFileSupport = !isEnableFileSupport
291
+ }
292
+ }
293
+
294
+ /// Return true if WKWebview supports handling this protocol, but WKWebview supports HTTP by default, so return false to support using custom HTTP Handler
295
+ @objc private dynamic
296
+ static func wrapHandles(urlScheme: String) -> Bool {
297
+ if urlScheme == "file" { return false }
298
+ return wrapHandles(urlScheme: urlScheme)
299
+ }
300
+ }
@@ -0,0 +1,34 @@
1
+ import SwiftUI
2
+ @preconcurrency import WebKit
3
+
4
+ struct SpatialWebView: UIViewRepresentable {
5
+ weak var model: SpatialWebViewModel? = nil
6
+ var url: URL = .init(filePath: "/")
7
+ private var webviewStateChangeInvoke: ((_ type: SpatialWebViewState) -> Void)?
8
+
9
+ func makeUIView(context: Context) -> WKWebView {
10
+ webviewStateChangeInvoke?(.didMakeView)
11
+ return model!.getController().webview!
12
+ }
13
+
14
+ func makeCoordinator() -> SpatialWebController {
15
+ return model!.getController()
16
+ }
17
+
18
+ func updateUIView(_ webView: WKWebView, context: Context) {
19
+ webviewStateChangeInvoke?(.didUpdateView)
20
+ }
21
+
22
+ mutating func registerWebviewStateChangeInvoke(invoke: @escaping (_ type: SpatialWebViewState) -> Void) {
23
+ webviewStateChangeInvoke = invoke
24
+ }
25
+
26
+ mutating func destroy() {
27
+ webviewStateChangeInvoke = nil
28
+ model = nil
29
+ }
30
+
31
+ static func dismantleUIView(_ uiView: WKWebView, coordinator: SpatialWebController) {
32
+ // print("dismantleUIView", coordinator.model?.id)
33
+ }
34
+ }