Skip to content

Commit 1f20006

Browse files
authored
Add SpriteKit traversal to session replay (#385)
1 parent 8b594fc commit 1f20006

File tree

4 files changed

+124
-3
lines changed

4 files changed

+124
-3
lines changed

platform/swift/source/replay/AnnotatedView.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import UIKit
1111
/// Defines the view types used to represent rectangles visually when rendering the session replay.
1212
///
1313
/// Note that as it's currently implemented, we only have 4 bits available and therefore we cannot exceed
14-
/// 15 types.
14+
/// 16 types.
1515
///
16-
/// Types left: 3
16+
/// Types left: 2
1717
public enum ViewType: UInt8 {
1818
case label = 0
1919
case button = 1
@@ -28,6 +28,7 @@ public enum ViewType: UInt8 {
2828
case transparentView = 10
2929
case keyboard = 11
3030
case webview = 12
31+
case sprite = 13
3132

3233
case ignore = 254
3334
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
import SpriteKit
9+
10+
extension Replay {
11+
/**
12+
* This function is used to traverse the view hierarchy of a SpriteKit scene and
13+
* collect the frames of all visible nodes. It takes a buffer to store the frames,
14+
* the SKView to traverse, the parent position of the nodes, and a clipping rectangle
15+
* to limit the area of interest.
16+
*
17+
* - parameter buffer: The buffer to store the frames of the nodes.
18+
* - parameter view: The SKView to traverse.
19+
* - parameter parentPosition: The parent position of the nodes.
20+
* - parameter clipTo: The clipping rectangle to limit the area of interest.
21+
*/
22+
func traverse(into buffer: inout Data, view: SKView, clipTo: CGRect) {
23+
guard let scene = view.scene else { return }
24+
25+
var children: [(z: CGFloat, annotated: AnnotatedView)] = []
26+
var queue = [(scene as SKNode, 0, 0, 0.0, clipTo)]
27+
while let (node, index, parentIndex, nodeZ, clipTo) = queue.popLast() {
28+
let annotated = node.annotated(in: view, from: scene)
29+
30+
if annotated.type != .ignore {
31+
if annotated.frame.intersects(clipTo) {
32+
var clipped = annotated
33+
clipped.frame = annotated.frame.intersection(clipTo)
34+
let childIndexOffset = CGFloat(parentIndex) * 100 + CGFloat(index)
35+
children.append((z: (nodeZ + node.zPosition) * 1000 + childIndexOffset, clipped))
36+
}
37+
}
38+
39+
if !annotated.recurse {
40+
continue
41+
}
42+
43+
for (i, child) in node.children.enumerated() {
44+
let clipTo = node is SKCropNode ? annotated.frame : clipTo
45+
queue.append((child, i, parentIndex + 1, nodeZ + node.zPosition, clipTo))
46+
}
47+
}
48+
49+
for (_, annotated) in children.sorted(by: { $0.z <= $1.z }) {
50+
rectToBytes(type: annotated.type, buffer: &buffer, frame: annotated.frame)
51+
}
52+
}
53+
}
54+
55+
private extension SKNode {
56+
func annotated(in view: SKView, from scene: SKScene) -> AnnotatedView {
57+
let pointInScene = self.parent?.convert(self.frame.origin, to: scene)
58+
let frameInScene = CGRect(origin: pointInScene ?? self.frame.origin, size: self.frame.size)
59+
let frameInView = (view as UIView).convert(frameInScene, from: scene as UICoordinateSpace)
60+
61+
if self.alpha < 0.1 || self.isHidden {
62+
return .ignored
63+
}
64+
65+
var annotatedView: AnnotatedView?
66+
if let identifiable = self as? ReplayIdentifiable {
67+
annotatedView = identifiable.identify(frame: frameInView)
68+
}
69+
70+
if let identifiable = self as? ReplaySpriteKitIdentifiable {
71+
annotatedView = identifiable.identifySpriteKit(frame: frameInView)
72+
}
73+
74+
return annotatedView ?? .skipped
75+
}
76+
}
77+
78+
// MARK: - SpriteKit default indentification
79+
80+
/// Internal protocol to provide SpriteKit identifications for known subclasses. This is split from
81+
/// `ReplayIdentifiable` mainly because we want consumers with subclasses of `SpriteKit` types to be able to
82+
/// define their types independently of our heuristic.
83+
protocol ReplaySpriteKitIdentifiable where Self: SKNode {
84+
/// This method works the same way as `ReplayIdentifiable.identify` refer to that for more information.
85+
///
86+
/// - parameter frame: See `ReplayIdentifiable.identify`.
87+
///
88+
/// - returns: See `ReplayIdentifiable.identify`.
89+
func identifySpriteKit(frame: CGRect) -> AnnotatedView?
90+
}
91+
92+
extension SKSpriteNode: ReplaySpriteKitIdentifiable {
93+
func identifySpriteKit(frame: CGRect) -> AnnotatedView? { AnnotatedView(.sprite, frame: frame) }
94+
}
95+
96+
extension SKShapeNode: ReplaySpriteKitIdentifiable {
97+
func identifySpriteKit(frame: CGRect) -> AnnotatedView? { AnnotatedView(.sprite, frame: frame) }
98+
}
99+
100+
extension SKEmitterNode: ReplaySpriteKitIdentifiable {
101+
func identifySpriteKit(frame: CGRect) -> AnnotatedView? { AnnotatedView(.sprite, frame: frame) }
102+
}
103+
104+
extension SKLabelNode: ReplaySpriteKitIdentifiable {
105+
func identifySpriteKit(frame: CGRect) -> AnnotatedView? { AnnotatedView(.label, frame: frame) }
106+
}
107+
108+
extension SKVideoNode: ReplaySpriteKitIdentifiable {
109+
func identifySpriteKit(frame: CGRect) -> AnnotatedView? { AnnotatedView(.image, frame: frame) }
110+
}
111+
112+
extension SKTileMapNode: ReplaySpriteKitIdentifiable {
113+
func identifySpriteKit(frame: CGRect) -> AnnotatedView? { AnnotatedView(.image, frame: frame) }
114+
}

platform/swift/source/replay/Replay.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
77

88
import Foundation
9+
import SpriteKit
910
import UIKit
1011

1112
private typealias CategorizerFunction = (UIView, CGRect) -> AnnotatedView?
@@ -81,6 +82,11 @@ package final class Replay {
8182
private func traverse(into buffer: inout Data, parent: UIView, parentPosition: CGPoint, clipTo: CGRect,
8283
ignoreViewType: Bool = false)
8384
{
85+
// Traverse SKView as a SpriteKit tree if parent view is SpriteKit
86+
if let view = parent as? SKView {
87+
self.traverse(into: &buffer, view: view, clipTo: clipTo)
88+
}
89+
8490
for view in parent.subviews {
8591
if view.isHidden || view.alpha < 0.1 {
8692
continue

platform/swift/source/replay/ReplayIdentifiable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import UIKit
2222
/// func identify(frame: inout CGRect) -> (type: ViewType, recurse: Bool)? { return (.label, false) }
2323
/// }
2424
/// ```
25-
public protocol ReplayIdentifiable where Self: UIView {
25+
public protocol ReplayIdentifiable {
2626
/// A function that returns the desired type for the receiver. This method can optionally re-defined
2727
/// the final frame. This is useful when the visual content does not match the receiver frame.
2828
/// For example, an ImageView with an image that is centered can set the frame for the image itself

0 commit comments

Comments
 (0)