@applicaster/quick-brick-native-apple 6.18.0 → 6.19.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "QuickBrickApple",
3
- "version": "6.18.0",
3
+ "version": "6.19.0",
4
4
  "platforms": {
5
5
  "ios": "16.0",
6
6
  "tvos": "16.0"
@@ -16,7 +16,7 @@
16
16
  "authors": "Applicaster LTD.",
17
17
  "source": {
18
18
  "git": "https://github.com/applicaster/Zapp-Frameworks.git",
19
- "tag": "@@applicaster/quick-brick-native-apple/6.18.0"
19
+ "tag": "@@applicaster/quick-brick-native-apple/6.19.0"
20
20
  },
21
21
  "requires_arc": true,
22
22
  "source_files": "universal/**/*.{m,swift}",
@@ -14,11 +14,16 @@ class GroupProxyCoordinator {
14
14
 
15
15
  protocol Command {
16
16
  var groupId: String { get }
17
+ var shouldDiscard: Bool { get }
17
18
  func canBeApplied() -> Bool
18
19
  func apply()
19
20
  func cancel()
20
21
  }
21
22
 
23
+ protocol QueueTimeoutCommand: Command {
24
+ func startQueueTimeout()
25
+ }
26
+
22
27
  func tryApply(_ command: Command) {
23
28
  lock.lock()
24
29
  defer { lock.unlock() }
@@ -27,6 +32,7 @@ class GroupProxyCoordinator {
27
32
  command.apply()
28
33
  } else {
29
34
  commandsQueue.append(command)
35
+ (command as? QueueTimeoutCommand)?.startQueueTimeout()
30
36
  }
31
37
  }
32
38
 
@@ -37,6 +43,10 @@ class GroupProxyCoordinator {
37
43
  lock.unlock()
38
44
 
39
45
  for command in commands {
46
+ if command.shouldDiscard {
47
+ continue
48
+ }
49
+
40
50
  if command.canBeApplied() {
41
51
  command.apply()
42
52
  } else {
@@ -9,7 +9,7 @@ import Foundation
9
9
  import UIKit
10
10
 
11
11
  extension GroupProxyCoordinator {
12
- class SetItemFocus: Command {
12
+ class SetItemFocus: Command, QueueTimeoutCommand {
13
13
  let groupId: String
14
14
  let itemId: String
15
15
  let needsForceUpdate: Bool
@@ -17,6 +17,15 @@ extension GroupProxyCoordinator {
17
17
  weak var manager: GroupProxyManager?
18
18
  private var isCancelled = false
19
19
  private let lock = NSLock()
20
+ private var queueTimeoutWorkItem: DispatchWorkItem?
21
+
22
+ private static let queueTimeout: TimeInterval = 5.0
23
+
24
+ var shouldDiscard: Bool {
25
+ lock.lock()
26
+ defer { lock.unlock() }
27
+ return isCancelled
28
+ }
20
29
 
21
30
  init(groupId: String,
22
31
  itemId: String,
@@ -30,6 +39,37 @@ extension GroupProxyCoordinator {
30
39
  self.manager = manager
31
40
  }
32
41
 
42
+ func startQueueTimeout() {
43
+ lock.lock()
44
+ defer { lock.unlock() }
45
+
46
+ guard !isCancelled else { return }
47
+
48
+ let workItem = DispatchWorkItem { [weak self] in
49
+ self?.handleQueueTimeout()
50
+ }
51
+ queueTimeoutWorkItem = workItem
52
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.queueTimeout, execute: workItem)
53
+ }
54
+
55
+ private func handleQueueTimeout() {
56
+ lock.lock()
57
+ if isCancelled {
58
+ lock.unlock()
59
+ return
60
+ }
61
+ isCancelled = true
62
+ queueTimeoutWorkItem = nil
63
+ lock.unlock()
64
+
65
+ completion?(false)
66
+ }
67
+
68
+ private func cancelQueueTimeout() {
69
+ queueTimeoutWorkItem?.cancel()
70
+ queueTimeoutWorkItem = nil
71
+ }
72
+
33
73
  func canBeApplied() -> Bool {
34
74
  lock.lock()
35
75
  defer { lock.unlock() }
@@ -65,9 +105,9 @@ extension GroupProxyCoordinator {
65
105
 
66
106
  func apply() {
67
107
  lock.lock()
108
+ cancelQueueTimeout()
68
109
  if isCancelled {
69
110
  lock.unlock()
70
- completion?(false)
71
111
  return
72
112
  }
73
113
  lock.unlock()
@@ -80,26 +120,41 @@ extension GroupProxyCoordinator {
80
120
  return
81
121
  }
82
122
 
83
- manager.setForceFocusUpdateData(itemId: itemId, groupId: groupId)
123
+ guard needsForceUpdate else {
124
+ let completionHandler = completion
125
+ DispatchQueue.main.async {
126
+ groupView.forceFocus(
127
+ view: focusableView,
128
+ needsForceUpdate: false
129
+ )
130
+ completionHandler?(true)
131
+ }
132
+ return
133
+ }
134
+
135
+ let completionHandler = completion
136
+ manager.setForceFocusUpdateData(itemId: itemId, groupId: groupId, callback: { success in
137
+ completionHandler?(success)
138
+ })
84
139
 
85
- DispatchQueue.main.async { [weak manager] in
140
+ DispatchQueue.main.async {
86
141
  groupView.forceFocus(
87
142
  view: focusableView,
88
- needsForceUpdate: self.needsForceUpdate,
89
- callback: { success in
90
- manager?.clearForceFocusUpdate()
91
- self.completion?(success)
92
- }
143
+ needsForceUpdate: true
93
144
  )
94
145
  }
95
146
  }
96
147
 
97
148
  func cancel() {
98
149
  lock.lock()
150
+ cancelQueueTimeout()
151
+ let wasAlreadyCancelled = isCancelled
99
152
  isCancelled = true
100
153
  lock.unlock()
101
154
 
102
- completion?(false)
155
+ if !wasAlreadyCancelled {
156
+ completion?(false)
157
+ }
103
158
  }
104
159
  }
105
160
  }
@@ -17,6 +17,12 @@ extension GroupProxyCoordinator {
17
17
  private var isCancelled = false
18
18
  private let lock = NSLock()
19
19
 
20
+ var shouldDiscard: Bool {
21
+ lock.lock()
22
+ defer { lock.unlock() }
23
+ return isCancelled
24
+ }
25
+
20
26
  init(groupId: String,
21
27
  itemId: String,
22
28
  isPreferred: Bool,
@@ -41,6 +47,10 @@ extension GroupProxyCoordinator {
41
47
  return false
42
48
  }
43
49
 
50
+ if group.isFocusDisabled {
51
+ return false
52
+ }
53
+
44
54
  if !proxy.isExistingChild(itemId) {
45
55
  return false
46
56
  }
@@ -71,8 +81,10 @@ extension GroupProxyCoordinator {
71
81
  return
72
82
  }
73
83
 
74
- DispatchQueue.main.async {
75
- if self.isPreferred {
84
+ let isPreferred = isPreferred
85
+ DispatchQueue.main.async { [weak self] in
86
+ guard self != nil else { return }
87
+ if isPreferred {
76
88
  group.setUserPreferredFocusEnvironments([focusableView])
77
89
  } else if let currentPreferred = group.getUserPreferredFocusEnvironments()?.first as? FocusableView,
78
90
  currentPreferred === focusableView {
@@ -26,7 +26,16 @@ class GroupProxy {
26
26
  lock.lock()
27
27
  defer { lock.unlock() }
28
28
 
29
- return children[itemId]?.value
29
+ guard let box = children[itemId] else {
30
+ return nil
31
+ }
32
+
33
+ guard let view = box.value else {
34
+ children.removeValue(forKey: itemId)
35
+ return nil
36
+ }
37
+
38
+ return view
30
39
  }
31
40
 
32
41
  // MARK: - Child Management
@@ -66,7 +75,17 @@ class GroupProxy {
66
75
  func isExistingChild(_ itemId: String) -> Bool {
67
76
  lock.lock()
68
77
  defer { lock.unlock() }
69
- return children[itemId]?.value != nil
78
+
79
+ guard let box = children[itemId] else {
80
+ return false
81
+ }
82
+
83
+ guard box.value != nil else {
84
+ children.removeValue(forKey: itemId)
85
+ return false
86
+ }
87
+
88
+ return true
70
89
  }
71
90
 
72
91
  // MARK: - Group Binding
@@ -101,7 +120,7 @@ class GroupProxy {
101
120
  coordinator?.onGroupBound(groupId: groupId)
102
121
  }
103
122
 
104
- func unbindGroup(expectedGroup: FocusableGroupView) {
123
+ @MainActor func unbindGroup(expectedGroup: FocusableGroupView) {
105
124
  if let group,
106
125
  group !== expectedGroup {
107
126
  logger?.warningLog(message: """
@@ -109,7 +128,7 @@ class GroupProxy {
109
128
  Expected group: \(expectedGroup.debugDescription)
110
129
  Current bound group: \(String(describing: group.debugDescription))
111
130
  identifier1: \(ObjectIdentifier(expectedGroup))
112
- identifier2: \(ObjectIdentifier(group)))
131
+ identifier2: \(ObjectIdentifier(group))
113
132
  Skipping unbind.
114
133
  """)
115
134
 
@@ -117,6 +136,7 @@ class GroupProxy {
117
136
  }
118
137
 
119
138
  lock.lock()
139
+ group = nil
120
140
  let shouldRemoveProxy = children.isEmpty
121
141
  lock.unlock()
122
142
 
@@ -124,10 +144,6 @@ class GroupProxy {
124
144
  coordinator?.cancelCommandsForGroup(groupId: groupId)
125
145
  GroupProxyManager.shared.removeProxy(forGroupId: groupId)
126
146
  }
127
-
128
- lock.lock()
129
- group = nil
130
- lock.unlock()
131
147
  }
132
148
 
133
149
  // MARK: - Preferred Focus
@@ -56,20 +56,72 @@ class GroupProxyManager {
56
56
  proxies.removeValue(forKey: groupId)
57
57
  }
58
58
 
59
- // MARK: - Focus Update
59
+ // MARK: - Force Focus Update
60
60
 
61
61
  private var forceFocusUpdateData: (itemId: String, groupId: String)?
62
+ private var forceFocusCallback: ((Bool) -> Void)?
62
63
 
63
- func setForceFocusUpdateData(itemId: String, groupId: String) {
64
+ private var forceFocusTimer: Timer?
65
+
66
+ func setForceFocusUpdateData(itemId: String, groupId: String, callback: ((Bool) -> Void)?) {
64
67
  lock.lock()
68
+
69
+ let previousCallback = forceFocusCallback
65
70
  forceFocusUpdateData = (itemId: itemId, groupId: groupId)
71
+ forceFocusCallback = callback
66
72
  lock.unlock()
73
+
74
+ DispatchQueue.main.async { [weak self] in
75
+ previousCallback?(false)
76
+
77
+ self?.forceFocusTimer?.invalidate()
78
+ self?.startForceFocusTimer()
79
+ }
80
+ }
81
+
82
+ private func startForceFocusTimer() {
83
+ let timeout: TimeInterval = 1
84
+ forceFocusTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
85
+ self?.handleForceFocusTimeout()
86
+ }
67
87
  }
68
88
 
69
- func clearForceFocusUpdate() {
89
+ private func handleForceFocusTimeout() {
90
+ forceFocusTimer = nil
91
+
70
92
  lock.lock()
93
+ let callback = forceFocusCallback
94
+ forceFocusCallback = nil
71
95
  forceFocusUpdateData = nil
72
96
  lock.unlock()
97
+
98
+ callback?(false)
99
+ }
100
+
101
+ @MainActor
102
+ func notifyFocusDidUpdate(focusedView: FocusableView?) {
103
+ lock.lock()
104
+
105
+ guard let focusData = forceFocusUpdateData,
106
+ let callback = forceFocusCallback else {
107
+ lock.unlock()
108
+ return
109
+ }
110
+
111
+ guard let focusedView,
112
+ focusedView.itemId == focusData.itemId,
113
+ focusedView.groupId == focusData.groupId else {
114
+ lock.unlock()
115
+ return
116
+ }
117
+
118
+ forceFocusCallback = nil
119
+ forceFocusUpdateData = nil
120
+ lock.unlock()
121
+
122
+ forceFocusTimer?.invalidate()
123
+ forceFocusTimer = nil
124
+ callback(true)
73
125
  }
74
126
 
75
127
  func focusableViewCanBecomeFocusableForceFocusUpdate(groupId: String?, itemId: String?) -> Bool? {
@@ -35,7 +35,6 @@ public class FocusableGroupView: RCTTVView {
35
35
  }
36
36
 
37
37
  public private(set) var isPresented: Bool = false
38
- private var focusTimer: Timer?
39
38
 
40
39
  override public func didMoveToWindow() {
41
40
  super.didMoveToWindow()
@@ -74,8 +73,6 @@ public class FocusableGroupView: RCTTVView {
74
73
  isGroupRegistered = true
75
74
  }
76
75
 
77
- public var didForceFocusCallBack: ((Bool) -> Void)?
78
-
79
76
  private let activeStateNotifier = FocusableGroupStateNotifierDefault()
80
77
 
81
78
  @objc public var onGroupFocus: RCTDirectEventBlock?
@@ -107,6 +104,9 @@ public class FocusableGroupView: RCTTVView {
107
104
  }
108
105
  }
109
106
 
107
+ /// Parent group ID for nested group hierarchy.
108
+ /// Currently used for informational purposes only (passed to React Native events).
109
+ /// If nested group registration is needed, implement didSet with proxy registration similar to FocusableView.
110
110
  @objc public var groupId: String?
111
111
 
112
112
  func sendGroupRegisteredEvent() {
@@ -138,69 +138,31 @@ public class FocusableGroupView: RCTTVView {
138
138
  }
139
139
 
140
140
  deinit {
141
- focusTimer?.invalidate()
142
- focusTimer = nil
143
141
  cleanup()
144
142
  }
145
143
 
146
144
  @MainActor public func forceFocus(view: FocusableView,
147
- needsForceUpdate: Bool,
148
- callback completion: ((Bool) -> Void)?) {
145
+ needsForceUpdate: Bool) {
149
146
  updatePreferredFocusEnv(with: view)
150
147
 
151
- if !needsForceUpdate {
152
- completion?(true)
148
+ guard needsForceUpdate else {
153
149
  return
154
150
  }
155
151
 
156
152
  guard let rootView = UIApplication.shared.rootViewController()?.view as? RCTRootView else {
157
- completion?(false)
158
153
  return
159
154
  }
160
155
 
161
156
  guard itemId != nil else {
162
- completion?(false)
163
157
  return
164
158
  }
165
159
 
166
- focusTimer?.invalidate()
167
- focusTimer = nil
168
-
169
- didForceFocusCallBack = { [weak self] success in
170
- guard let self else {
171
- completion?(false)
172
- return
173
- }
174
-
175
- focusTimer?.invalidate()
176
- focusTimer = nil
177
-
178
- completion?(success)
179
- didForceFocusCallBack = nil
180
- }
181
-
182
- let cancelationTimeout: TimeInterval = 1
183
- focusTimer?.invalidate()
184
- focusTimer = Timer.scheduledTimer(withTimeInterval: cancelationTimeout, repeats: false) { [weak self] _ in
185
- guard let self else {
186
- completion?(false)
187
- return
188
- }
189
-
190
- if let callback = didForceFocusCallBack {
191
- callback(false)
192
- didForceFocusCallBack = nil
193
- }
194
-
195
- focusTimer = nil
196
- }
197
-
198
160
  rootView.setNeedsFocusUpdate()
199
161
  rootView.updateFocusIfNeeded()
200
162
  }
201
163
 
202
164
  private func cleanup() {
203
- guard itemId != nil else {
165
+ guard isGroupRegistered else {
204
166
  return
205
167
  }
206
168
 
@@ -365,7 +327,7 @@ public class FocusableGroupView: RCTTVView {
365
327
  updatePreferredFocusView(nextFocusItem: context.nextFocusedItem)
366
328
  }
367
329
 
368
- didForceFocusCallBack?(true)
330
+ GroupProxyManager.shared.notifyFocusDidUpdate(focusedView: context.nextFocusedView as? FocusableView)
369
331
 
370
332
  let isActive = focusItemIsDescendant(nextFocusItem: context.nextFocusedItem)
371
333
  activeStateNotifier.updateFocus(currentlyActive: isActive, context: context)
@@ -373,7 +335,7 @@ public class FocusableGroupView: RCTTVView {
373
335
  }
374
336
 
375
337
  extension FocusableGroupView: FocusableGroupProtocol {
376
- var canBecomeFocusable: Bool {
338
+ var canChildrenBecomeFocused: Bool {
377
339
  if let forceFocusEnabled = GroupProxyManager.shared.focusableGroupCanBecomeFocusableForceFocusUpdate(groupId: itemId) {
378
340
  return forceFocusEnabled
379
341
  }
@@ -112,11 +112,12 @@ public class FocusableView: ParallaxView {
112
112
  delegate = self
113
113
  }
114
114
 
115
- @objc private(set) var itemId: String? {
115
+ @objc public var itemId: String? {
116
116
  didSet {
117
117
  if itemId != oldValue {
118
118
  if oldValue != nil {
119
119
  proxy?.removeChild(self)
120
+ proxy = nil
120
121
  isViewRegistered = false
121
122
  }
122
123
 
@@ -125,7 +126,7 @@ public class FocusableView: ParallaxView {
125
126
  }
126
127
  }
127
128
 
128
- @objc private(set) var groupId: String? {
129
+ @objc public var groupId: String? {
129
130
  didSet {
130
131
  if groupId != oldValue {
131
132
  if oldValue != nil {
@@ -172,8 +173,13 @@ public class FocusableView: ParallaxView {
172
173
  }
173
174
 
174
175
  private func cleanup() {
176
+ guard isViewRegistered else {
177
+ return
178
+ }
179
+
175
180
  proxy?.removeChild(self)
176
181
  proxy = nil
182
+ isViewRegistered = false
177
183
  }
178
184
 
179
185
  private func addFocusGuideIfNeeded(tag: NSNumber?,
@@ -258,6 +264,6 @@ public class FocusableView: ParallaxView {
258
264
  return forceFocusEnabled
259
265
  }
260
266
 
261
- return focusableGroup.canBecomeFocusable && focusable
267
+ return focusableGroup.canChildrenBecomeFocused && focusable
262
268
  }
263
269
  }
@@ -6,6 +6,6 @@
6
6
  //
7
7
 
8
8
  protocol FocusableGroupProtocol: AnyObject {
9
- var canBecomeFocusable: Bool { get }
9
+ var canChildrenBecomeFocused: Bool { get }
10
10
  var itemId: String? { get }
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-native-apple",
3
- "version": "6.18.0",
3
+ "version": "6.19.0",
4
4
  "description": "iOS and tvOS native code for QuickBrick applications. This package is used to provide native logic for QuickBrick",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"