@onekeyfe/react-native-perp-depth-bar 6.7.0-12 → 6.7.0-13
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.
package/PerpDepthBar.podspec
CHANGED
|
@@ -16,8 +16,11 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.source_files = [
|
|
17
17
|
"ios/**/*.{swift}",
|
|
18
18
|
"ios/**/*.{m,mm}",
|
|
19
|
+
"ios/PerpDepthBarInterop.hpp",
|
|
19
20
|
"cpp/**/*.{hpp,cpp}",
|
|
20
21
|
]
|
|
22
|
+
s.exclude_files = "ios/tests/**/*"
|
|
23
|
+
s.public_header_files = "ios/PerpDepthBarInterop.hpp"
|
|
21
24
|
|
|
22
25
|
s.dependency 'React-jsi'
|
|
23
26
|
s.dependency 'React-callinvoker'
|
|
@@ -26,4 +29,11 @@ Pod::Spec.new do |s|
|
|
|
26
29
|
add_nitrogen_files(s)
|
|
27
30
|
|
|
28
31
|
install_modules_dependencies(s)
|
|
32
|
+
|
|
33
|
+
s.test_spec "Tests" do |test_spec|
|
|
34
|
+
test_spec.source_files = "ios/tests/**/*.swift"
|
|
35
|
+
test_spec.pod_target_xcconfig = {
|
|
36
|
+
"SWIFT_OBJC_INTEROP_MODE" => "objcxx"
|
|
37
|
+
}
|
|
38
|
+
end
|
|
29
39
|
end
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Native iOS and Android views for an order-book depth column and a bid/ask ratio
|
|
|
6
6
|
track. Each view owns native drawing and animation; the application provides
|
|
7
7
|
formatted prices, sizes and percentages.
|
|
8
8
|
|
|
9
|
+
The native ownership and update contract is in [docs/SPEC.md](docs/SPEC.md).
|
|
10
|
+
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
11
13
|
```sh
|
|
@@ -37,7 +37,16 @@ final class HybridPerpDepthBars: HybridPerpDepthBarsSpec {
|
|
|
37
37
|
private var hasLaidOut = false
|
|
38
38
|
|
|
39
39
|
// MARK: - Props (required props -> non-optional in generated spec)
|
|
40
|
-
var
|
|
40
|
+
private var imperativeDepth = false
|
|
41
|
+
private var displayPercents: [Double] = []
|
|
42
|
+
var percents: [Double] {
|
|
43
|
+
get { displayPercents }
|
|
44
|
+
set {
|
|
45
|
+
guard !imperativeDepth else { return }
|
|
46
|
+
displayPercents = newValue
|
|
47
|
+
scheduleLayout()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
41
50
|
var rowHeight: Double = 0 { didSet { scheduleLayout() } }
|
|
42
51
|
var rowMarginTop: Double = 0 { didSet { scheduleLayout() } }
|
|
43
52
|
var barInset: Double = 0 { didSet { scheduleLayout() } }
|
|
@@ -56,8 +65,25 @@ final class HybridPerpDepthBars: HybridPerpDepthBarsSpec {
|
|
|
56
65
|
private var cachedColor: CGColor = UIColor.clear.cgColor
|
|
57
66
|
|
|
58
67
|
// MARK: - Text props
|
|
59
|
-
var
|
|
60
|
-
var
|
|
68
|
+
private var imperativeText = false
|
|
69
|
+
private var displayPrices: [String] = []
|
|
70
|
+
private var displaySizes: [String] = []
|
|
71
|
+
var prices: [String] {
|
|
72
|
+
get { displayPrices }
|
|
73
|
+
set {
|
|
74
|
+
guard !imperativeText else { return }
|
|
75
|
+
displayPrices = newValue
|
|
76
|
+
scheduleLayout()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var sizes: [String] {
|
|
80
|
+
get { displaySizes }
|
|
81
|
+
set {
|
|
82
|
+
guard !imperativeText else { return }
|
|
83
|
+
displaySizes = newValue
|
|
84
|
+
scheduleLayout()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
61
87
|
var priceFontSize: Double = 11 { didSet { scheduleLayout() } }
|
|
62
88
|
var sizeFontSize: Double = 11 { didSet { scheduleLayout() } }
|
|
63
89
|
var textInset: Double = 0 { didSet { scheduleLayout() } }
|
|
@@ -111,32 +137,52 @@ final class HybridPerpDepthBars: HybridPerpDepthBarsSpec {
|
|
|
111
137
|
/// path the prop uses — only the data source differs.
|
|
112
138
|
func setDepth(buffer: ArrayBuffer) throws {
|
|
113
139
|
let count = buffer.size / 4
|
|
114
|
-
guard count > 0 else {
|
|
115
|
-
percents = []
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
// `buffer.data` is a `UInt8` pointer; reinterpret it as little-endian
|
|
119
|
-
// Float32. iOS (arm/x86) is little-endian and JS `Float32Array` is too, so
|
|
120
|
-
// an unaligned little-endian load matches the prop semantics exactly.
|
|
121
140
|
var depths = [Double](repeating: 0, count: count)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
if count > 0 {
|
|
142
|
+
// `buffer.data` is a `UInt8` pointer; reinterpret it as little-endian
|
|
143
|
+
// Float32. iOS (arm/x86) is little-endian and JS `Float32Array` is too, so
|
|
144
|
+
// an unaligned little-endian load matches the prop semantics exactly.
|
|
145
|
+
let raw = UnsafeRawPointer(buffer.data)
|
|
146
|
+
for i in 0..<count {
|
|
147
|
+
let bits = raw.loadUnaligned(fromByteOffset: i * 4, as: UInt32.self)
|
|
148
|
+
depths[i] = Double(Float(bitPattern: UInt32(littleEndian: bits)))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Finish reading the caller's buffer before publishing Swift-owned values.
|
|
152
|
+
// Layout and its layer state are owned by the main thread.
|
|
153
|
+
if Thread.isMainThread {
|
|
154
|
+
publishDepth(depths)
|
|
155
|
+
} else {
|
|
156
|
+
DispatchQueue.main.async { [weak self] in self?.publishDepth(depths) }
|
|
126
157
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func publishDepth(_ depths: [Double]) {
|
|
161
|
+
imperativeDepth = true
|
|
162
|
+
displayPercents = depths
|
|
163
|
+
scheduleLayout()
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
/// Imperatively update the per-row price/size text — companion to `setDepth`
|
|
133
167
|
/// for the text layer. Bypasses the high-frequency `prices`/`sizes` prop write
|
|
134
|
-
/// path;
|
|
135
|
-
///
|
|
168
|
+
/// path; publication feeds through the same `scheduleLayout` → `performLayout`
|
|
169
|
+
/// path the prop setters use (REACT-NATIVE-1JZ). Call in the
|
|
136
170
|
/// SAME frame as `setDepth` so bar fill and row text share one source frame.
|
|
137
171
|
func setText(prices: [String], sizes: [String]) throws {
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
if Thread.isMainThread {
|
|
173
|
+
publishText(prices: prices, sizes: sizes)
|
|
174
|
+
} else {
|
|
175
|
+
DispatchQueue.main.async { [weak self] in
|
|
176
|
+
self?.publishText(prices: prices, sizes: sizes)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func publishText(prices: [String], sizes: [String]) {
|
|
182
|
+
imperativeText = true
|
|
183
|
+
displayPrices = prices
|
|
184
|
+
displaySizes = sizes
|
|
185
|
+
scheduleLayout()
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
private func handleTap(atY y: CGFloat) {
|
|
@@ -150,8 +196,7 @@ final class HybridPerpDepthBars: HybridPerpDepthBarsSpec {
|
|
|
150
196
|
}
|
|
151
197
|
|
|
152
198
|
private func scheduleLayout() {
|
|
153
|
-
//
|
|
154
|
-
// must be issued on the main thread.
|
|
199
|
+
// UIKit layout requests belong on the main thread.
|
|
155
200
|
if Thread.isMainThread {
|
|
156
201
|
view.setNeedsLayout()
|
|
157
202
|
} else {
|
|
@@ -66,8 +66,16 @@ final class HybridPerpSideRatio: HybridPerpSideRatioSpec {
|
|
|
66
66
|
/// `scheduleLayout` → `performLayout` clamp/split/display-link path the prop
|
|
67
67
|
/// setters use — only the data source differs.
|
|
68
68
|
func setRatio(bidPercentage: Double, askPercentage: Double) throws {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
if Thread.isMainThread {
|
|
70
|
+
self.bidPercentage = bidPercentage
|
|
71
|
+
self.askPercentage = askPercentage
|
|
72
|
+
} else {
|
|
73
|
+
DispatchQueue.main.async { [weak self] in
|
|
74
|
+
guard let self else { return }
|
|
75
|
+
self.bidPercentage = bidPercentage
|
|
76
|
+
self.askPercentage = askPercentage
|
|
77
|
+
}
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
private func scheduleLayout() {
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import UIKit
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
@testable import PerpDepthBar
|
|
6
|
+
|
|
7
|
+
@MainActor
|
|
8
|
+
final class HybridPerpDepthBarsTests: XCTestCase {
|
|
9
|
+
func testOffMainUpdatesWaitForMainAndSnapshotBufferBytes() throws {
|
|
10
|
+
let depth = makeDepthView()
|
|
11
|
+
depth.percents = [10]
|
|
12
|
+
depth.prices = ["old price"]
|
|
13
|
+
depth.sizes = ["old size"]
|
|
14
|
+
depth.view.layoutIfNeeded()
|
|
15
|
+
|
|
16
|
+
let buffer = makeBuffer([25, 75])
|
|
17
|
+
runOffMain {
|
|
18
|
+
try! depth.setDepth(buffer: buffer)
|
|
19
|
+
try! depth.setText(prices: ["25", "75"], sizes: ["2.5"])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The worker returned while the main queue was blocked by runOffMain.
|
|
23
|
+
XCTAssertEqual(depth.percents, [10])
|
|
24
|
+
XCTAssertEqual(depth.prices, ["old price"])
|
|
25
|
+
XCTAssertEqual(depth.sizes, ["old size"])
|
|
26
|
+
|
|
27
|
+
write([90, 90], into: buffer)
|
|
28
|
+
drainMainQueue()
|
|
29
|
+
XCTAssertEqual(depth.percents, [25, 75])
|
|
30
|
+
XCTAssertEqual(depth.prices, ["25", "75"])
|
|
31
|
+
XCTAssertEqual(depth.sizes, ["2.5"])
|
|
32
|
+
|
|
33
|
+
depth.view.layoutIfNeeded()
|
|
34
|
+
let bars = barLayers(in: depth)
|
|
35
|
+
XCTAssertEqual(bars.count, 2)
|
|
36
|
+
XCTAssertEqual(Double(bars[0].transform.m11), 0.25, accuracy: 0.001)
|
|
37
|
+
XCTAssertEqual(Double(bars[1].transform.m11), 0.75, accuracy: 0.001)
|
|
38
|
+
XCTAssertEqual(textStrings(in: depth), ["25", "2.5", "75", ""])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testOffMainEmptyDepthClearsOnlyAfterMainPublication() throws {
|
|
42
|
+
let depth = makeDepthView()
|
|
43
|
+
depth.percents = [60]
|
|
44
|
+
depth.prices = ["60"]
|
|
45
|
+
depth.sizes = ["6"]
|
|
46
|
+
depth.placeholderText = "--"
|
|
47
|
+
depth.placeholderRows = 2
|
|
48
|
+
depth.view.layoutIfNeeded()
|
|
49
|
+
|
|
50
|
+
let emptyBuffer = makeBuffer([])
|
|
51
|
+
runOffMain {
|
|
52
|
+
try! depth.setDepth(buffer: emptyBuffer)
|
|
53
|
+
try! depth.setText(prices: [], sizes: [])
|
|
54
|
+
}
|
|
55
|
+
XCTAssertEqual(depth.percents, [60])
|
|
56
|
+
XCTAssertEqual(depth.prices, ["60"])
|
|
57
|
+
XCTAssertEqual(depth.sizes, ["6"])
|
|
58
|
+
|
|
59
|
+
drainMainQueue()
|
|
60
|
+
depth.view.layoutIfNeeded()
|
|
61
|
+
XCTAssertTrue(depth.percents.isEmpty)
|
|
62
|
+
XCTAssertTrue(depth.prices.isEmpty)
|
|
63
|
+
XCTAssertTrue(depth.sizes.isEmpty)
|
|
64
|
+
XCTAssertEqual(barLayers(in: depth).count, 2)
|
|
65
|
+
XCTAssertEqual(textStrings(in: depth), ["--", "--", "--", "--"])
|
|
66
|
+
XCTAssertTrue(barLayers(in: depth).allSatisfy { $0.transform.m11 == 0 })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func testRapidGrowAndShrinkPublishInOrderWithRealLayers() throws {
|
|
70
|
+
let depth = makeDepthView()
|
|
71
|
+
depth.percents = [5]
|
|
72
|
+
depth.prices = ["initial"]
|
|
73
|
+
depth.sizes = ["initial"]
|
|
74
|
+
|
|
75
|
+
let grow = makeBuffer([10, 20, 30])
|
|
76
|
+
let shrink = makeBuffer([40])
|
|
77
|
+
let growAgain = makeBuffer([70, 80])
|
|
78
|
+
runOffMain {
|
|
79
|
+
try! depth.setDepth(buffer: grow)
|
|
80
|
+
try! depth.setText(prices: ["a", "b", "c"], sizes: ["1", "2", "3"])
|
|
81
|
+
try! depth.setDepth(buffer: shrink)
|
|
82
|
+
try! depth.setText(prices: ["d"], sizes: ["4"])
|
|
83
|
+
try! depth.setDepth(buffer: growAgain)
|
|
84
|
+
try! depth.setText(prices: ["e", "f"], sizes: ["7", "8"])
|
|
85
|
+
}
|
|
86
|
+
XCTAssertEqual(depth.percents, [5])
|
|
87
|
+
XCTAssertEqual(depth.prices, ["initial"])
|
|
88
|
+
XCTAssertEqual(depth.sizes, ["initial"])
|
|
89
|
+
|
|
90
|
+
drainMainQueue()
|
|
91
|
+
depth.view.layoutIfNeeded()
|
|
92
|
+
XCTAssertEqual(depth.percents, [70, 80])
|
|
93
|
+
XCTAssertEqual(depth.prices, ["e", "f"])
|
|
94
|
+
XCTAssertEqual(depth.sizes, ["7", "8"])
|
|
95
|
+
XCTAssertEqual(barLayers(in: depth).count, 2)
|
|
96
|
+
XCTAssertEqual(textStrings(in: depth), ["e", "7", "f", "8"])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func testMainThreadUpdatesRemainSynchronous() throws {
|
|
100
|
+
let depth = makeDepthView()
|
|
101
|
+
try depth.setDepth(buffer: makeBuffer([30, 50]))
|
|
102
|
+
try depth.setText(prices: ["p1", "p2"], sizes: ["s1", "s2"])
|
|
103
|
+
|
|
104
|
+
XCTAssertEqual(depth.percents, [30, 50])
|
|
105
|
+
XCTAssertEqual(depth.prices, ["p1", "p2"])
|
|
106
|
+
XCTAssertEqual(depth.sizes, ["s1", "s2"])
|
|
107
|
+
depth.view.layoutIfNeeded()
|
|
108
|
+
XCTAssertEqual(barLayers(in: depth).count, 2)
|
|
109
|
+
XCTAssertEqual(textStrings(in: depth), ["p1", "s1", "p2", "s2"])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func testImperativeDepthIgnoresLaterPropsWithoutTakingTextOwnership() throws {
|
|
113
|
+
let depth = makeDepthView()
|
|
114
|
+
depth.percents = [10]
|
|
115
|
+
depth.percents = [20]
|
|
116
|
+
XCTAssertEqual(depth.percents, [20])
|
|
117
|
+
|
|
118
|
+
try depth.setDepth(buffer: makeBuffer([35]))
|
|
119
|
+
depth.percents = []
|
|
120
|
+
depth.percents = [99, 99]
|
|
121
|
+
depth.prices = ["prop price"]
|
|
122
|
+
depth.sizes = ["prop size"]
|
|
123
|
+
depth.view.layoutIfNeeded()
|
|
124
|
+
|
|
125
|
+
XCTAssertEqual(depth.percents, [35])
|
|
126
|
+
XCTAssertEqual(barLayers(in: depth).count, 1)
|
|
127
|
+
XCTAssertEqual(Double(barLayers(in: depth)[0].transform.m11), 0.35, accuracy: 0.001)
|
|
128
|
+
XCTAssertEqual(textStrings(in: depth), ["prop price", "prop size"])
|
|
129
|
+
|
|
130
|
+
try depth.setDepth(buffer: makeBuffer([]))
|
|
131
|
+
depth.percents = [99]
|
|
132
|
+
depth.view.layoutIfNeeded()
|
|
133
|
+
XCTAssertTrue(depth.percents.isEmpty)
|
|
134
|
+
XCTAssertTrue(barLayers(in: depth).isEmpty)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func testImperativeTextIgnoresLaterPropsWithoutTakingDepthOwnership() throws {
|
|
138
|
+
let depth = makeDepthView()
|
|
139
|
+
depth.prices = ["initial"]
|
|
140
|
+
depth.sizes = ["initial"]
|
|
141
|
+
depth.prices = ["updated"]
|
|
142
|
+
depth.sizes = ["updated"]
|
|
143
|
+
XCTAssertEqual(depth.prices, ["updated"])
|
|
144
|
+
XCTAssertEqual(depth.sizes, ["updated"])
|
|
145
|
+
|
|
146
|
+
runOffMain {
|
|
147
|
+
try! depth.setText(prices: ["imperative price"], sizes: ["imperative size"])
|
|
148
|
+
}
|
|
149
|
+
drainMainQueue()
|
|
150
|
+
depth.prices = []
|
|
151
|
+
depth.sizes = []
|
|
152
|
+
depth.prices = ["stale price"]
|
|
153
|
+
depth.sizes = ["stale size"]
|
|
154
|
+
depth.percents = [40]
|
|
155
|
+
depth.view.layoutIfNeeded()
|
|
156
|
+
|
|
157
|
+
XCTAssertEqual(depth.percents, [40])
|
|
158
|
+
XCTAssertEqual(textStrings(in: depth), ["imperative price", "imperative size"])
|
|
159
|
+
|
|
160
|
+
try depth.setText(prices: [], sizes: [])
|
|
161
|
+
depth.prices = ["stale"]
|
|
162
|
+
depth.sizes = ["stale"]
|
|
163
|
+
depth.view.layoutIfNeeded()
|
|
164
|
+
XCTAssertTrue(depth.prices.isEmpty)
|
|
165
|
+
XCTAssertTrue(depth.sizes.isEmpty)
|
|
166
|
+
XCTAssertEqual(textStrings(in: depth), ["", ""])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func testOffMainDepthSurvivesLaterFabricPropWrites() {
|
|
170
|
+
let depth = makeDepthView()
|
|
171
|
+
let buffer = makeBuffer([25, 75])
|
|
172
|
+
runOffMain {
|
|
173
|
+
try! depth.setDepth(buffer: buffer)
|
|
174
|
+
}
|
|
175
|
+
drainMainQueue()
|
|
176
|
+
depth.percents = []
|
|
177
|
+
depth.view.layoutIfNeeded()
|
|
178
|
+
XCTAssertEqual(depth.percents, [25, 75])
|
|
179
|
+
XCTAssertEqual(barLayers(in: depth).count, 2)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func testRatioPairPublishesTogetherOnMainWithRealLayers() throws {
|
|
183
|
+
let ratio = HybridPerpSideRatio()
|
|
184
|
+
ratio.view.frame = CGRect(x: 0, y: 0, width: 202, height: 20)
|
|
185
|
+
ratio.reducedMotion = true
|
|
186
|
+
ratio.view.layoutIfNeeded()
|
|
187
|
+
|
|
188
|
+
runOffMain {
|
|
189
|
+
try! ratio.setRatio(bidPercentage: 20, askPercentage: 80)
|
|
190
|
+
try! ratio.setRatio(bidPercentage: 70, askPercentage: 30)
|
|
191
|
+
}
|
|
192
|
+
XCTAssertEqual(ratio.bidPercentage, 50)
|
|
193
|
+
XCTAssertEqual(ratio.askPercentage, 50)
|
|
194
|
+
drainMainQueue()
|
|
195
|
+
ratio.view.layoutIfNeeded()
|
|
196
|
+
XCTAssertEqual(ratio.bidPercentage, 70)
|
|
197
|
+
XCTAssertEqual(ratio.askPercentage, 30)
|
|
198
|
+
let layers = try XCTUnwrap(ratio.view.layer.sublayers)
|
|
199
|
+
XCTAssertEqual(layers[0].frame.width, 140, accuracy: 0.001)
|
|
200
|
+
XCTAssertEqual(layers[1].frame.width, 60, accuracy: 0.001)
|
|
201
|
+
|
|
202
|
+
try ratio.setRatio(bidPercentage: 30, askPercentage: 70)
|
|
203
|
+
XCTAssertEqual(ratio.bidPercentage, 30)
|
|
204
|
+
XCTAssertEqual(ratio.askPercentage, 70)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func testQueuedRatioUpdateDoesNotRetainDisposedView() {
|
|
208
|
+
var ratio: HybridPerpSideRatio? = HybridPerpSideRatio()
|
|
209
|
+
weak var weakRatio = ratio
|
|
210
|
+
runOffMain {
|
|
211
|
+
try! ratio?.setRatio(bidPercentage: 20, askPercentage: 80)
|
|
212
|
+
}
|
|
213
|
+
ratio = nil
|
|
214
|
+
XCTAssertNil(weakRatio)
|
|
215
|
+
drainMainQueue()
|
|
216
|
+
XCTAssertNil(weakRatio)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func testQueuedUpdateDoesNotRetainDisposedView() {
|
|
220
|
+
final class OwnerBox {
|
|
221
|
+
var owner: HybridPerpDepthBars?
|
|
222
|
+
|
|
223
|
+
init(_ owner: HybridPerpDepthBars) {
|
|
224
|
+
self.owner = owner
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let box = OwnerBox(makeDepthView())
|
|
229
|
+
weak var weakOwner: HybridPerpDepthBars?
|
|
230
|
+
weakOwner = box.owner
|
|
231
|
+
let buffer = makeBuffer([35])
|
|
232
|
+
runOffMain {
|
|
233
|
+
let owner = box.owner
|
|
234
|
+
try! owner?.setDepth(buffer: buffer)
|
|
235
|
+
try! owner?.setText(prices: ["35"], sizes: ["3.5"])
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
box.owner = nil
|
|
239
|
+
XCTAssertNil(weakOwner)
|
|
240
|
+
drainMainQueue()
|
|
241
|
+
XCTAssertNil(weakOwner)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private func makeDepthView() -> HybridPerpDepthBars {
|
|
245
|
+
let depth = HybridPerpDepthBars()
|
|
246
|
+
depth.view.frame = CGRect(x: 0, y: 0, width: 240, height: 160)
|
|
247
|
+
depth.rowHeight = 28
|
|
248
|
+
depth.rowMarginTop = 4
|
|
249
|
+
depth.reducedMotion = true
|
|
250
|
+
return depth
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func makeBuffer(_ values: [Float]) -> ArrayBuffer {
|
|
254
|
+
let buffer = ArrayBuffer.allocate(size: values.count * MemoryLayout<UInt32>.size)
|
|
255
|
+
write(values, into: buffer)
|
|
256
|
+
return buffer
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private func write(_ values: [Float], into buffer: ArrayBuffer) {
|
|
260
|
+
precondition(buffer.size == values.count * MemoryLayout<UInt32>.size)
|
|
261
|
+
for (index, value) in values.enumerated() {
|
|
262
|
+
var bits = value.bitPattern.littleEndian
|
|
263
|
+
withUnsafeBytes(of: &bits) { bytes in
|
|
264
|
+
for byte in 0..<MemoryLayout<UInt32>.size {
|
|
265
|
+
buffer.data[index * MemoryLayout<UInt32>.size + byte] = bytes[byte]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private func runOffMain(_ update: @escaping () -> Void) {
|
|
272
|
+
let finished = DispatchSemaphore(value: 0)
|
|
273
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
274
|
+
update()
|
|
275
|
+
finished.signal()
|
|
276
|
+
}
|
|
277
|
+
XCTAssertEqual(finished.wait(timeout: .now() + 2), .success)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func drainMainQueue() {
|
|
281
|
+
let published = expectation(description: "main-queue depth publication")
|
|
282
|
+
DispatchQueue.main.async { published.fulfill() }
|
|
283
|
+
wait(for: [published], timeout: 2)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func barLayers(in depth: HybridPerpDepthBars) -> [CALayer] {
|
|
287
|
+
(depth.view.layer.sublayers ?? []).filter { !($0 is CATextLayer) }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private func textStrings(in depth: HybridPerpDepthBars) -> [String] {
|
|
291
|
+
(depth.view.layer.sublayers ?? [])
|
|
292
|
+
.compactMap { $0 as? CATextLayer }
|
|
293
|
+
.compactMap { $0.string as? String }
|
|
294
|
+
}
|
|
295
|
+
}
|
package/package.json
CHANGED