@applicaster/quick-brick-native-apple 6.16.0 → 6.17.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 (25) hide show
  1. package/apple/QuickBrickApple.podspec.json +2 -2
  2. package/apple/ios/ReactNative/PushBridge.swift +2 -2
  3. package/apple/ios/ReactNative/QuickBrickViewController.swift +3 -3
  4. package/apple/tvos/Helpers/FocusableGroupManager/Coordinator/GroupProxyCoordinator.swift +70 -0
  5. package/apple/tvos/Helpers/FocusableGroupManager/Coordinator/SetItemFocus.swift +105 -0
  6. package/apple/tvos/Helpers/FocusableGroupManager/Coordinator/SetPreferredFocus.swift +90 -0
  7. package/apple/tvos/Helpers/FocusableGroupManager/FocusableManagerModule.swift +1 -2
  8. package/apple/tvos/Helpers/FocusableGroupManager/GroupProxy.swift +82 -42
  9. package/apple/tvos/Helpers/FocusableGroupManager/GroupProxyManager.swift +79 -49
  10. package/apple/tvos/ReactNative/QuickBrickViewController.swift +2 -2
  11. package/apple/tvos/ReactNativeModulesExportstvOS.m +2 -0
  12. package/apple/tvos/Views/FocusableGroupView/FocusableGroupStateNotifier.swift +2 -5
  13. package/apple/tvos/Views/FocusableGroupView/FocusableGroupView.swift +134 -34
  14. package/apple/tvos/Views/FocusableView/FocusableView.swift +32 -1
  15. package/apple/universal/ReactNative/AnalyticsBridge.swift +2 -2
  16. package/apple/universal/ReactNative/AppLoaderBridge.swift +14 -14
  17. package/apple/universal/ReactNative/EventBusBridge.swift +2 -2
  18. package/apple/universal/ReactNative/LocalNotification/LocalNotificationBridge.swift +4 -4
  19. package/apple/universal/ReactNative/OfflineAssetsBridge.swift +11 -11
  20. package/apple/universal/ReactNative/PluginsManagerBridge.swift +17 -17
  21. package/apple/universal/ReactNative/QuickBrickExceptionManagerDelegate.swift +2 -2
  22. package/apple/universal/ReactNative/ReactNativeCommunicationModule.swift +10 -10
  23. package/apple/universal/Storages/LocalStorage/LocalStorageBridge.swift +29 -29
  24. package/apple/universal/Storages/SessionStorage/SessionStorageBridge.swift +15 -15
  25. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "QuickBrickApple",
3
- "version": "6.16.0",
3
+ "version": "6.17.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.16.0"
19
+ "tag": "@@applicaster/quick-brick-native-apple/6.17.0"
20
20
  },
21
21
  "requires_arc": true,
22
22
  "source_files": "universal/**/*.{m,swift}",
@@ -21,12 +21,12 @@ class PushBridge: NSObject, RCTBridgeModule {
21
21
  "PushBridge"
22
22
  }
23
23
 
24
- public class func requiresMainQueueSetup() -> Bool {
24
+ class func requiresMainQueueSetup() -> Bool {
25
25
  true
26
26
  }
27
27
 
28
28
  /// prefered thread on which to run this native module
29
- @objc public var methodQueue: DispatchQueue {
29
+ @objc var methodQueue: DispatchQueue {
30
30
  DispatchQueue.main
31
31
  }
32
32
 
@@ -20,7 +20,7 @@ class QuickBrickViewController: UIViewController, UILayerViewControllerProtocol
20
20
 
21
21
  var orientationMask: UIInterfaceOrientationMask = QuickBrickViewController.initialOrientationMask
22
22
 
23
- override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
23
+ override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
24
24
  orientationMask
25
25
  }
26
26
 
@@ -28,7 +28,7 @@ class QuickBrickViewController: UIViewController, UILayerViewControllerProtocol
28
28
  homeIndicatorAutoHidden
29
29
  }
30
30
 
31
- override public var shouldAutorotate: Bool {
31
+ override var shouldAutorotate: Bool {
32
32
  true
33
33
  }
34
34
 
@@ -64,7 +64,7 @@ class QuickBrickViewController: UIViewController, UILayerViewControllerProtocol
64
64
  UIDevice.current.orientation
65
65
  }
66
66
 
67
- public func allowOrientationForScreen(_ orientation: ReactNativeOrientation) {
67
+ func allowOrientationForScreen(_ orientation: ReactNativeOrientation) {
68
68
  orientationMask = orientation.toInterfaceOrientationMask()
69
69
  forceOrientationWithMaskIfNeeded(orientationMask)
70
70
  }
@@ -0,0 +1,70 @@
1
+ //
2
+ // GroupProxyCoordinator.swift
3
+ // QuickBrickApple
4
+ //
5
+ // Created by Anton Kononenko on 12/22/25.
6
+ //
7
+
8
+ import Foundation
9
+ import React
10
+
11
+ class GroupProxyCoordinator {
12
+ private var commandsQueue: [Command] = []
13
+ private let lock = NSLock()
14
+
15
+ protocol Command {
16
+ var groupId: String { get }
17
+ func canBeApplied() -> Bool
18
+ func apply()
19
+ func cancel()
20
+ }
21
+
22
+ func tryApply(_ command: Command) {
23
+ lock.lock()
24
+ defer { lock.unlock() }
25
+
26
+ if command.canBeApplied() {
27
+ command.apply()
28
+ } else {
29
+ commandsQueue.append(command)
30
+ }
31
+ }
32
+
33
+ private func processQueue() {
34
+ lock.lock()
35
+ let commands = commandsQueue
36
+ commandsQueue.removeAll()
37
+ lock.unlock()
38
+
39
+ for command in commands {
40
+ if command.canBeApplied() {
41
+ command.apply()
42
+ } else {
43
+ lock.lock()
44
+ commandsQueue.append(command)
45
+ lock.unlock()
46
+ }
47
+ }
48
+ }
49
+
50
+ func onGroupBound(groupId _: String) {
51
+ processQueue()
52
+ }
53
+
54
+ func onChildAdded(groupId _: String, itemId _: String) {
55
+ processQueue()
56
+ }
57
+
58
+ func cancelCommandsForGroup(groupId: String) {
59
+ lock.lock()
60
+ defer { lock.unlock() }
61
+
62
+ commandsQueue.removeAll { command in
63
+ if command.groupId == groupId {
64
+ command.cancel()
65
+ return true
66
+ }
67
+ return false
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,105 @@
1
+ //
2
+ // SetItemFocus.swift
3
+ // QuickBrickApple
4
+ //
5
+ // Created by Anton Kononenko on 12/22/25.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ extension GroupProxyCoordinator {
12
+ class SetItemFocus: Command {
13
+ let groupId: String
14
+ let itemId: String
15
+ let needsForceUpdate: Bool
16
+ let completion: ((Bool) -> Void)?
17
+ weak var manager: GroupProxyManager?
18
+ private var isCancelled = false
19
+ private let lock = NSLock()
20
+
21
+ init(groupId: String,
22
+ itemId: String,
23
+ needsForceUpdate: Bool,
24
+ completion: ((Bool) -> Void)?,
25
+ manager: GroupProxyManager) {
26
+ self.groupId = groupId
27
+ self.itemId = itemId
28
+ self.needsForceUpdate = needsForceUpdate
29
+ self.completion = completion
30
+ self.manager = manager
31
+ }
32
+
33
+ func canBeApplied() -> Bool {
34
+ lock.lock()
35
+ defer { lock.unlock() }
36
+
37
+ if isCancelled {
38
+ return false
39
+ }
40
+
41
+ guard let manager,
42
+ let proxy = manager.getProxy(forGroupId: groupId),
43
+ let group = proxy.getGroup() else {
44
+ return false
45
+ }
46
+
47
+ if group.isFocusDisabled {
48
+ return false
49
+ }
50
+
51
+ if !proxy.isExistingChild(itemId) {
52
+ return false
53
+ }
54
+
55
+ guard let focusableView = proxy.getChild(by: itemId) else {
56
+ return false
57
+ }
58
+
59
+ if focusableView.window == nil || focusableView.frame.isEmpty {
60
+ return false
61
+ }
62
+
63
+ return true
64
+ }
65
+
66
+ func apply() {
67
+ lock.lock()
68
+ if isCancelled {
69
+ lock.unlock()
70
+ completion?(false)
71
+ return
72
+ }
73
+ lock.unlock()
74
+
75
+ guard let manager,
76
+ let proxy = manager.getProxy(forGroupId: groupId),
77
+ let groupView = proxy.getGroup(),
78
+ let focusableView = proxy.getChild(by: itemId) else {
79
+ completion?(false)
80
+ return
81
+ }
82
+
83
+ manager.setForceFocusUpdateData(itemId: itemId, groupId: groupId)
84
+
85
+ DispatchQueue.main.async { [weak manager] in
86
+ groupView.forceFocus(
87
+ view: focusableView,
88
+ needsForceUpdate: self.needsForceUpdate,
89
+ callback: { success in
90
+ manager?.clearForceFocusUpdate()
91
+ self.completion?(success)
92
+ }
93
+ )
94
+ }
95
+ }
96
+
97
+ func cancel() {
98
+ lock.lock()
99
+ isCancelled = true
100
+ lock.unlock()
101
+
102
+ completion?(false)
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,90 @@
1
+ //
2
+ // SetPreferredFocus.swift
3
+ // QuickBrickApple
4
+ //
5
+ // Created by Anton Kononenko on 12/23/25.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ extension GroupProxyCoordinator {
12
+ class SetPreferredFocus: Command {
13
+ let groupId: String
14
+ let itemId: String
15
+ let isPreferred: Bool
16
+ weak var manager: GroupProxyManager?
17
+ private var isCancelled = false
18
+ private let lock = NSLock()
19
+
20
+ init(groupId: String,
21
+ itemId: String,
22
+ isPreferred: Bool,
23
+ manager: GroupProxyManager) {
24
+ self.groupId = groupId
25
+ self.itemId = itemId
26
+ self.isPreferred = isPreferred
27
+ self.manager = manager
28
+ }
29
+
30
+ func canBeApplied() -> Bool {
31
+ lock.lock()
32
+ defer { lock.unlock() }
33
+
34
+ if isCancelled {
35
+ return false
36
+ }
37
+
38
+ guard let manager,
39
+ let proxy = manager.getProxy(forGroupId: groupId),
40
+ let group = proxy.getGroup() else {
41
+ return false
42
+ }
43
+
44
+ if !proxy.isExistingChild(itemId) {
45
+ return false
46
+ }
47
+
48
+ guard let focusableView = proxy.getChild(by: itemId) else {
49
+ return false
50
+ }
51
+
52
+ if focusableView.window == nil || focusableView.frame.isEmpty {
53
+ return false
54
+ }
55
+
56
+ return true
57
+ }
58
+
59
+ func apply() {
60
+ lock.lock()
61
+ if isCancelled {
62
+ lock.unlock()
63
+ return
64
+ }
65
+ lock.unlock()
66
+
67
+ guard let manager,
68
+ let proxy = manager.getProxy(forGroupId: groupId),
69
+ let group = proxy.getGroup(),
70
+ let focusableView = proxy.getChild(by: itemId) else {
71
+ return
72
+ }
73
+
74
+ DispatchQueue.main.async {
75
+ if self.isPreferred {
76
+ group.setUserPreferredFocusEnvironments([focusableView])
77
+ } else if let currentPreferred = group.getUserPreferredFocusEnvironments()?.first as? FocusableView,
78
+ currentPreferred === focusableView {
79
+ group.setUserPreferredFocusEnvironments(nil)
80
+ }
81
+ }
82
+ }
83
+
84
+ func cancel() {
85
+ lock.lock()
86
+ isCancelled = true
87
+ lock.unlock()
88
+ }
89
+ }
90
+ }
@@ -15,7 +15,7 @@ let kFocusableManagerModule = "FocusableManagerModule"
15
15
  class FocusableManagerModule: NSObject, RCTBridgeModule {
16
16
  /// Delay timer before focus will be invoked, was done to resolve async problems.
17
17
  /// Sometimes focusale item did has superview when it render that may cayse glitches.
18
- var delayTimer = 100
18
+ var delayTimer = 0
19
19
 
20
20
  /// main React bridge
21
21
  var bridge: RCTBridge?
@@ -58,7 +58,6 @@ class FocusableManagerModule: NSObject, RCTBridgeModule {
58
58
  @objc func forceFocus(_ groupId: String?, itemId: String?, callback: RCTResponseSenderBlock?) {
59
59
  // We need delay to make sure that on native side group will have superview
60
60
  let delay = DispatchTime.now() + DispatchTimeInterval.milliseconds(delayTimer)
61
-
62
61
  DispatchQueue.main.asyncAfter(deadline: delay) {
63
62
  GroupProxyManager.shared.updateFocus(groupId: groupId,
64
63
  itemId: itemId,
@@ -11,50 +11,80 @@ import XrayLogger
11
11
  class GroupProxy {
12
12
  let groupId: String
13
13
  private weak var group: FocusableGroupView?
14
- private var children: [WeakBox<FocusableView>] = []
14
+ private weak var coordinator: GroupProxyCoordinator?
15
+ private var children: [String: WeakBox<FocusableView>] = [:]
15
16
  private let lock = NSLock()
16
17
 
17
- var delayedPreferredFocus: FocusableView?
18
-
19
18
  private lazy var logger = Logger.getLogger(for: "QuickBrickApple/FocusableGroupManager/GroupProxy")
20
19
 
21
- init(groupId: String) {
20
+ init(groupId: String, coordinator: GroupProxyCoordinator) {
22
21
  self.groupId = groupId
22
+ self.coordinator = coordinator
23
+ }
24
+
25
+ func getChild(by itemId: String) -> FocusableView? {
26
+ lock.lock()
27
+ defer { lock.unlock() }
28
+
29
+ return children[itemId]?.value
23
30
  }
24
31
 
25
32
  // MARK: - Child Management
26
33
 
27
34
  func addChild(_ view: FocusableView) {
28
35
  lock.lock()
29
- children.append(WeakBox(view))
36
+
37
+ guard let itemId = view.itemId else {
38
+ lock.unlock()
39
+ return
40
+ }
41
+
42
+ children[itemId] = WeakBox(view)
30
43
  lock.unlock()
44
+
45
+ view.sendViewRegisteredEvent()
46
+
47
+ coordinator?.onChildAdded(groupId: groupId, itemId: itemId)
31
48
  }
32
49
 
33
50
  func removeChild(_ view: FocusableView) {
34
51
  lock.lock()
35
- defer { lock.unlock() }
36
52
 
37
- children.removeAll { $0.value === view }
53
+ if let itemId = view.itemId {
54
+ children.removeValue(forKey: itemId)
55
+ }
38
56
 
39
- if children.isEmpty, group == nil {
57
+ let shouldRemoveProxy = children.isEmpty && group == nil
58
+ lock.unlock()
59
+
60
+ if shouldRemoveProxy {
61
+ coordinator?.cancelCommandsForGroup(groupId: groupId)
40
62
  GroupProxyManager.shared.removeProxy(forGroupId: groupId)
41
63
  }
64
+ }
42
65
 
43
- if delayedPreferredFocus === view {
44
- delayedPreferredFocus = nil
45
- }
66
+ func isExistingChild(_ itemId: String) -> Bool {
67
+ lock.lock()
68
+ defer { lock.unlock() }
69
+ return children[itemId]?.value != nil
46
70
  }
47
71
 
48
72
  // MARK: - Group Binding
49
73
 
50
74
  @MainActor func bindGroup(_ group: FocusableGroupView) {
51
- // Check for duplicate group binding
52
- if let existingGroup = self.group {
75
+ lock.lock()
76
+ let existingGroup = self.group
77
+ lock.unlock()
78
+
79
+ if let existingGroup, existingGroup !== group {
53
80
  logger?.warningLog(message: """
54
81
  ⚠️ Duplicate group binding detected for groupId: '\(groupId)'
55
82
  Previous group: \(existingGroup.debugDescription)
56
83
  New group: \(group.debugDescription)
57
- Overwriting previous binding.
84
+ identifier1: \(ObjectIdentifier(existingGroup))
85
+ identifier2: \(ObjectIdentifier(group))
86
+ Previous group will be unable to unbind (React Native created duplicate itemIds).
87
+ Old group's cleanup() will handle releasing its proxy reference.
58
88
  """)
59
89
 
60
90
  #if DEBUG
@@ -62,53 +92,63 @@ class GroupProxy {
62
92
  #endif
63
93
  }
64
94
 
95
+ lock.lock()
65
96
  self.group = group
97
+ lock.unlock()
66
98
 
67
- if let preferredFocusView = delayedPreferredFocus {
68
- applyPreferredFocus(group,
69
- view: preferredFocusView)
70
- delayedPreferredFocus = nil
71
- }
99
+ group.sendGroupRegisteredEvent()
100
+
101
+ coordinator?.onGroupBound(groupId: groupId)
72
102
  }
73
103
 
74
- func unbindGroup() {
104
+ func unbindGroup(expectedGroup: FocusableGroupView) {
105
+ if let group,
106
+ group !== expectedGroup {
107
+ logger?.warningLog(message: """
108
+ ⚠️ Mismatched group unbinding detected for groupId: '\(groupId)'
109
+ Expected group: \(expectedGroup.debugDescription)
110
+ Current bound group: \(String(describing: group.debugDescription))
111
+ identifier1: \(ObjectIdentifier(expectedGroup))
112
+ identifier2: \(ObjectIdentifier(group)))
113
+ Skipping unbind.
114
+ """)
115
+
116
+ return
117
+ }
118
+
75
119
  lock.lock()
76
- defer { lock.unlock() }
120
+ let shouldRemoveProxy = children.isEmpty
121
+ lock.unlock()
77
122
 
78
- if children.isEmpty {
123
+ if shouldRemoveProxy {
124
+ coordinator?.cancelCommandsForGroup(groupId: groupId)
79
125
  GroupProxyManager.shared.removeProxy(forGroupId: groupId)
80
126
  }
81
127
 
128
+ lock.lock()
82
129
  group = nil
130
+ lock.unlock()
83
131
  }
84
132
 
85
133
  // MARK: - Preferred Focus
86
134
 
87
- @MainActor private func applyPreferredFocus(_ currentGroup: FocusableGroupView, view: FocusableView) {
88
- if view.preferredFocus {
89
- currentGroup.setUserPreferredFocusEnvironments([view])
90
- } else if let currentPreferred = currentGroup.getUserPreferredFocusEnvironments()?.first as? FocusableView,
91
- currentPreferred === view {
92
- currentGroup.setUserPreferredFocusEnvironments(nil)
135
+ @MainActor func updatePreferredFocus(_ view: FocusableView) {
136
+ guard let itemId = view.itemId else {
137
+ return
93
138
  }
94
- }
95
139
 
96
- @MainActor func updatePreferredFocus(_ view: FocusableView) {
97
- lock.lock()
98
- let currentGroup = group
99
- lock.unlock()
140
+ coordinator?.cancelCommandsForGroup(groupId: groupId)
100
141
 
101
- if let currentGroup {
102
- applyPreferredFocus(currentGroup,
103
- view: view)
104
- } else {
105
- if view.preferredFocus {
106
- delayedPreferredFocus = view
107
- }
108
- }
142
+ let command = GroupProxyCoordinator.SetPreferredFocus(
143
+ groupId: groupId,
144
+ itemId: itemId,
145
+ isPreferred: view.preferredFocus,
146
+ manager: GroupProxyManager.shared
147
+ )
148
+
149
+ coordinator?.tryApply(command)
109
150
  }
110
151
 
111
- /// Get the bound group (if any)
112
152
  func getGroup() -> FocusableGroupView? {
113
153
  lock.lock()
114
154
  defer { lock.unlock() }