@applicaster/quick-brick-native-apple 6.16.1 → 6.18.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.
- package/apple/QuickBrickApple.podspec.json +2 -2
- package/apple/tvos/Helpers/FocusableGroupManager/Coordinator/GroupProxyCoordinator.swift +70 -0
- package/apple/tvos/Helpers/FocusableGroupManager/Coordinator/SetItemFocus.swift +105 -0
- package/apple/tvos/Helpers/FocusableGroupManager/Coordinator/SetPreferredFocus.swift +90 -0
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableManagerModule.swift +1 -2
- package/apple/tvos/Helpers/FocusableGroupManager/GroupProxy.swift +82 -42
- package/apple/tvos/Helpers/FocusableGroupManager/GroupProxyManager.swift +79 -49
- package/apple/tvos/ReactNativeModulesExportstvOS.m +2 -0
- package/apple/tvos/Views/FocusableGroupView/FocusableGroupStateNotifier.swift +2 -5
- package/apple/tvos/Views/FocusableGroupView/FocusableGroupView.swift +134 -34
- package/apple/tvos/Views/FocusableView/FocusableView.swift +32 -1
- package/apple/universal/ReactNative/ContextResolverBridge.swift +77 -0
- package/apple/universal/ReactNative/ReactNativeModulesExports.m +11 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "QuickBrickApple",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.18.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.
|
|
19
|
+
"tag": "@@applicaster/quick-brick-native-apple/6.18.0"
|
|
20
20
|
},
|
|
21
21
|
"requires_arc": true,
|
|
22
22
|
"source_files": "universal/**/*.{m,swift}",
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
if let itemId = view.itemId {
|
|
54
|
+
children.removeValue(forKey: itemId)
|
|
55
|
+
}
|
|
38
56
|
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
120
|
+
let shouldRemoveProxy = children.isEmpty
|
|
121
|
+
lock.unlock()
|
|
77
122
|
|
|
78
|
-
if
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
lock.lock()
|
|
98
|
-
let currentGroup = group
|
|
99
|
-
lock.unlock()
|
|
140
|
+
coordinator?.cancelCommandsForGroup(groupId: groupId)
|
|
100
141
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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() }
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import Foundation
|
|
9
9
|
import React
|
|
10
|
+
import ZappCore
|
|
10
11
|
|
|
11
12
|
class GroupProxyManager {
|
|
12
13
|
static let shared = GroupProxyManager()
|
|
13
14
|
|
|
14
15
|
private let lock = NSLock()
|
|
15
|
-
private var proxies: [GroupProxy] = []
|
|
16
|
-
|
|
16
|
+
private var proxies: [String: GroupProxy] = [:]
|
|
17
|
+
private var coordinator: GroupProxyCoordinator = .init()
|
|
17
18
|
private init() {}
|
|
18
19
|
|
|
19
20
|
// MARK: - Proxy Access
|
|
@@ -22,26 +23,89 @@ class GroupProxyManager {
|
|
|
22
23
|
lock.lock()
|
|
23
24
|
defer { lock.unlock() }
|
|
24
25
|
|
|
25
|
-
if let existing = proxies
|
|
26
|
+
if let existing = proxies[groupId] {
|
|
26
27
|
return existing
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
let proxy = GroupProxy(groupId: groupId)
|
|
30
|
-
proxies
|
|
30
|
+
let proxy = GroupProxy(groupId: groupId, coordinator: coordinator)
|
|
31
|
+
proxies[groupId] = proxy
|
|
31
32
|
return proxy
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
func getProxy(forGroupId groupId: String) -> GroupProxy? {
|
|
36
|
+
lock.lock()
|
|
37
|
+
defer { lock.unlock() }
|
|
38
|
+
|
|
39
|
+
return proxies[groupId]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func hasProxy(forGroupId groupId: String) -> Bool {
|
|
43
|
+
lock.lock()
|
|
44
|
+
defer { lock.unlock() }
|
|
45
|
+
|
|
46
|
+
return proxies[groupId] != nil
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
// MARK: - Cleanup
|
|
35
50
|
|
|
36
51
|
func removeProxy(forGroupId groupId: String) {
|
|
37
52
|
lock.lock()
|
|
38
53
|
defer { lock.unlock() }
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
coordinator.cancelCommandsForGroup(groupId: groupId)
|
|
56
|
+
proxies.removeValue(forKey: groupId)
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
// MARK: - Focus Update
|
|
44
60
|
|
|
61
|
+
private var forceFocusUpdateData: (itemId: String, groupId: String)?
|
|
62
|
+
|
|
63
|
+
func setForceFocusUpdateData(itemId: String, groupId: String) {
|
|
64
|
+
lock.lock()
|
|
65
|
+
forceFocusUpdateData = (itemId: itemId, groupId: groupId)
|
|
66
|
+
lock.unlock()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func clearForceFocusUpdate() {
|
|
70
|
+
lock.lock()
|
|
71
|
+
forceFocusUpdateData = nil
|
|
72
|
+
lock.unlock()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func focusableViewCanBecomeFocusableForceFocusUpdate(groupId: String?, itemId: String?) -> Bool? {
|
|
76
|
+
lock.lock()
|
|
77
|
+
defer { lock.unlock() }
|
|
78
|
+
|
|
79
|
+
guard let groupId,
|
|
80
|
+
let itemId,
|
|
81
|
+
let forceFocusData = forceFocusUpdateData else {
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if groupId == forceFocusData.groupId,
|
|
86
|
+
itemId == forceFocusData.itemId {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func focusableGroupCanBecomeFocusableForceFocusUpdate(groupId: String?) -> Bool? {
|
|
94
|
+
lock.lock()
|
|
95
|
+
defer { lock.unlock() }
|
|
96
|
+
|
|
97
|
+
guard let groupId,
|
|
98
|
+
let forceFocusData = forceFocusUpdateData else {
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if groupId == forceFocusData.groupId {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
45
109
|
func updateFocus(groupId: String?,
|
|
46
110
|
itemId: String?,
|
|
47
111
|
needsForceUpdate: Bool = false,
|
|
@@ -52,50 +116,16 @@ class GroupProxyManager {
|
|
|
52
116
|
return
|
|
53
117
|
}
|
|
54
118
|
|
|
55
|
-
|
|
56
|
-
let proxy = proxies.first(where: { $0.groupId == groupId })
|
|
57
|
-
lock.unlock()
|
|
119
|
+
coordinator.cancelCommandsForGroup(groupId: groupId)
|
|
58
120
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
121
|
+
let command = GroupProxyCoordinator.SetItemFocus(
|
|
122
|
+
groupId: groupId,
|
|
123
|
+
itemId: itemId,
|
|
124
|
+
needsForceUpdate: needsForceUpdate,
|
|
125
|
+
completion: completion,
|
|
126
|
+
manager: self
|
|
127
|
+
)
|
|
64
128
|
|
|
65
|
-
|
|
66
|
-
var rootView: UIView? = groupView
|
|
67
|
-
|
|
68
|
-
while let unwrapedRootView = rootView,
|
|
69
|
-
!unwrapedRootView.isReactRootView() {
|
|
70
|
-
rootView = rootView?.superview
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
guard let rootViewUnwrapped = rootView,
|
|
74
|
-
let superView = rootViewUnwrapped.superview as? RCTRootView else {
|
|
75
|
-
completion?(false)
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if needsForceUpdate {
|
|
80
|
-
superView.setNeedsFocusUpdate()
|
|
81
|
-
superView.updateFocusIfNeeded()
|
|
82
|
-
|
|
83
|
-
let timeout: TimeInterval = 2.0
|
|
84
|
-
|
|
85
|
-
groupView.didFocusCallBack = (completion: {
|
|
86
|
-
completion?(true)
|
|
87
|
-
groupView.didFocusCallBack = nil
|
|
88
|
-
}, focusableItemId: itemId)
|
|
89
|
-
|
|
90
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
|
|
91
|
-
if groupView.didFocusCallBack != nil {
|
|
92
|
-
completion?(false)
|
|
93
|
-
groupView.didFocusCallBack = nil
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
completion?(true)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
129
|
+
coordinator.tryApply(command)
|
|
100
130
|
}
|
|
101
131
|
}
|
|
@@ -18,6 +18,7 @@ RCT_EXTERN_METHOD(setPreferredFocus:(NSString *)groupId itemId:(NSString *)itemI
|
|
|
18
18
|
@end
|
|
19
19
|
|
|
20
20
|
@interface RCT_EXTERN_MODULE(FocusableGroupViewModule, RCTViewManager)
|
|
21
|
+
RCT_EXPORT_VIEW_PROPERTY(onRegistered, RCTDirectEventBlock)
|
|
21
22
|
RCT_EXPORT_VIEW_PROPERTY(onGroupFocus, RCTDirectEventBlock)
|
|
22
23
|
RCT_EXPORT_VIEW_PROPERTY(onGroupBlur, RCTDirectEventBlock)
|
|
23
24
|
RCT_EXPORT_VIEW_PROPERTY(itemId, NSString);
|
|
@@ -27,6 +28,7 @@ RCT_EXPORT_VIEW_PROPERTY(isFocusDisabled, BOOL);
|
|
|
27
28
|
@end
|
|
28
29
|
|
|
29
30
|
@interface RCT_EXTERN_MODULE(FocusableViewModule, RCTViewManager)
|
|
31
|
+
RCT_EXPORT_VIEW_PROPERTY(onRegistered, RCTDirectEventBlock)
|
|
30
32
|
RCT_EXPORT_VIEW_PROPERTY(itemId, NSString);
|
|
31
33
|
RCT_EXPORT_VIEW_PROPERTY(groupId, NSString);
|
|
32
34
|
RCT_EXPORT_VIEW_PROPERTY(forceFocus, BOOL)
|
|
@@ -20,10 +20,7 @@ typealias NotifierActionType = (_ type: FocusableGroupNotifierActionType, _ cont
|
|
|
20
20
|
class FocusableGroupStateNotifierDefault: FocusableGroupStateNotifier {
|
|
21
21
|
private(set) var isActive: Bool = false
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
init(action: @escaping NotifierActionType) {
|
|
25
|
-
self.action = action
|
|
26
|
-
}
|
|
23
|
+
var action: NotifierActionType?
|
|
27
24
|
|
|
28
25
|
func updateFocus(currentlyActive: Bool, context: UIFocusUpdateContext) {
|
|
29
26
|
guard currentlyActive != isActive else { return }
|
|
@@ -33,6 +30,6 @@ class FocusableGroupStateNotifierDefault: FocusableGroupStateNotifier {
|
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
func sendNotifierEvent(context: UIFocusUpdateContext) {
|
|
36
|
-
action(isActive ? .onFocus : .onBlur, context)
|
|
33
|
+
action?(isActive ? .onFocus : .onBlur, context)
|
|
37
34
|
}
|
|
38
35
|
}
|
|
@@ -19,6 +19,7 @@ public class FocusableGroupView: RCTTVView {
|
|
|
19
19
|
let preferredFocusDisabledDesc = isPreferredFocusDisabled ? "preferred-focus-disabled" : "preferred-focus-enabled"
|
|
20
20
|
let dependentGroupsDesc = dependantGroupIds?.joined(separator: ",") ?? "NO_DEPS"
|
|
21
21
|
let reactTagDesc = reactTag?.stringValue ?? "NO_TAG"
|
|
22
|
+
let preferredFocusEnvDesc = preferredFocusEnvironments.first.map { String(describing: $0) } ?? "N/A"
|
|
22
23
|
|
|
23
24
|
return """
|
|
24
25
|
FocusableGroupView[
|
|
@@ -28,16 +29,58 @@ public class FocusableGroupView: RCTTVView {
|
|
|
28
29
|
STATE: \(activeStateDesc), \(focusDisabledDesc), \(preferredFocusDisabledDesc)
|
|
29
30
|
DEPS: \(dependentGroupsDesc)
|
|
30
31
|
FRAME: \(frame)
|
|
32
|
+
PREFFERED_FOCUS_ENVIRONMENTS: \(preferredFocusEnvDesc)
|
|
31
33
|
]
|
|
32
34
|
"""
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
public var
|
|
37
|
+
public private(set) var isPresented: Bool = false
|
|
38
|
+
private var focusTimer: Timer?
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
override public func didMoveToWindow() {
|
|
41
|
+
super.didMoveToWindow()
|
|
42
|
+
updateIsPresented()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override public func layoutSubviews() {
|
|
46
|
+
super.layoutSubviews()
|
|
47
|
+
updateIsPresented()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func updateIsPresented() {
|
|
51
|
+
isPresented =
|
|
52
|
+
window != nil &&
|
|
53
|
+
bounds.width > 0 &&
|
|
54
|
+
bounds.height > 0 &&
|
|
55
|
+
!isHidden &&
|
|
56
|
+
alpha > 0.01
|
|
57
|
+
registerGroup()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private func registerGroup() {
|
|
61
|
+
guard let itemId,
|
|
62
|
+
isPresented,
|
|
63
|
+
isGroupRegistered == false
|
|
64
|
+
else {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
activeStateNotifier.action = { [weak self] type, context in
|
|
69
|
+
self?.notifyReactNativeFocusUpdate(type, context)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
proxy = GroupProxyManager.shared.getOrCreateProxy(forGroupId: itemId)
|
|
73
|
+
proxy?.bindGroup(self)
|
|
74
|
+
isGroupRegistered = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public var didForceFocusCallBack: ((Bool) -> Void)?
|
|
78
|
+
|
|
79
|
+
private let activeStateNotifier = FocusableGroupStateNotifierDefault()
|
|
38
80
|
|
|
39
81
|
@objc public var onGroupFocus: RCTDirectEventBlock?
|
|
40
82
|
@objc public var onGroupBlur: RCTDirectEventBlock?
|
|
83
|
+
@objc public var onRegistered: RCTDirectEventBlock?
|
|
41
84
|
|
|
42
85
|
@objc public var isFocusDisabled: Bool = false
|
|
43
86
|
|
|
@@ -48,24 +91,33 @@ public class FocusableGroupView: RCTTVView {
|
|
|
48
91
|
isWithMemory == false
|
|
49
92
|
}
|
|
50
93
|
|
|
94
|
+
private var isGroupRegistered: Bool = false
|
|
95
|
+
|
|
51
96
|
@objc public var itemId: String? {
|
|
52
97
|
didSet {
|
|
53
98
|
if itemId != oldValue {
|
|
54
99
|
if oldValue != nil {
|
|
55
|
-
proxy?.unbindGroup()
|
|
100
|
+
proxy?.unbindGroup(expectedGroup: self)
|
|
56
101
|
proxy = nil
|
|
102
|
+
isGroupRegistered = false
|
|
57
103
|
}
|
|
58
104
|
|
|
59
|
-
|
|
60
|
-
proxy = GroupProxyManager.shared.getOrCreateProxy(forGroupId: newId)
|
|
61
|
-
proxy?.bindGroup(self)
|
|
62
|
-
}
|
|
105
|
+
registerGroup()
|
|
63
106
|
}
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
|
|
67
110
|
@objc public var groupId: String?
|
|
68
111
|
|
|
112
|
+
func sendGroupRegisteredEvent() {
|
|
113
|
+
var params: [String: Any] = [:]
|
|
114
|
+
params[GroupViewUpdateEvents.preferredFocusEnvironment] = dataForFocusItem(focusItem: preferredFocusEnvironments.first)
|
|
115
|
+
params[GroupViewUpdateEvents.groupId] = groupId
|
|
116
|
+
params[GroupViewUpdateEvents.isFocusDisabled] = isFocusDisabled
|
|
117
|
+
params[GroupViewUpdateEvents.itemId] = itemId
|
|
118
|
+
onRegistered?(params)
|
|
119
|
+
}
|
|
120
|
+
|
|
69
121
|
@MainActor private var userPreferredFocusEnvironments: [UIFocusEnvironment]?
|
|
70
122
|
@MainActor private var customPreferredFocusEnvironment: [UIFocusEnvironment]?
|
|
71
123
|
|
|
@@ -86,16 +138,79 @@ public class FocusableGroupView: RCTTVView {
|
|
|
86
138
|
}
|
|
87
139
|
|
|
88
140
|
deinit {
|
|
141
|
+
focusTimer?.invalidate()
|
|
142
|
+
focusTimer = nil
|
|
89
143
|
cleanup()
|
|
90
144
|
}
|
|
91
145
|
|
|
146
|
+
@MainActor public func forceFocus(view: FocusableView,
|
|
147
|
+
needsForceUpdate: Bool,
|
|
148
|
+
callback completion: ((Bool) -> Void)?) {
|
|
149
|
+
updatePreferredFocusEnv(with: view)
|
|
150
|
+
|
|
151
|
+
if !needsForceUpdate {
|
|
152
|
+
completion?(true)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
guard let rootView = UIApplication.shared.rootViewController()?.view as? RCTRootView else {
|
|
157
|
+
completion?(false)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
guard itemId != nil else {
|
|
162
|
+
completion?(false)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
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
|
+
rootView.setNeedsFocusUpdate()
|
|
199
|
+
rootView.updateFocusIfNeeded()
|
|
200
|
+
}
|
|
201
|
+
|
|
92
202
|
private func cleanup() {
|
|
93
|
-
guard
|
|
203
|
+
guard itemId != nil else {
|
|
94
204
|
return
|
|
95
205
|
}
|
|
96
206
|
|
|
97
|
-
|
|
207
|
+
activeStateNotifier.action = nil
|
|
208
|
+
customPreferredFocusEnvironment = nil
|
|
209
|
+
userPreferredFocusEnvironments = nil
|
|
210
|
+
|
|
211
|
+
proxy?.unbindGroup(expectedGroup: self)
|
|
98
212
|
proxy = nil
|
|
213
|
+
isGroupRegistered = false
|
|
99
214
|
}
|
|
100
215
|
|
|
101
216
|
public required init?(coder aDecoder: NSCoder) {
|
|
@@ -234,7 +349,9 @@ public class FocusableGroupView: RCTTVView {
|
|
|
234
349
|
// MARK: Focus Engine
|
|
235
350
|
|
|
236
351
|
override public var preferredFocusEnvironments: [UIFocusEnvironment] {
|
|
237
|
-
customPreferredFocusEnvironment ??
|
|
352
|
+
customPreferredFocusEnvironment ??
|
|
353
|
+
userPreferredFocusEnvironments ??
|
|
354
|
+
super.preferredFocusEnvironments
|
|
238
355
|
}
|
|
239
356
|
|
|
240
357
|
override public func shouldUpdateFocus(in _: UIFocusUpdateContext) -> Bool {
|
|
@@ -248,36 +365,19 @@ public class FocusableGroupView: RCTTVView {
|
|
|
248
365
|
updatePreferredFocusView(nextFocusItem: context.nextFocusedItem)
|
|
249
366
|
}
|
|
250
367
|
|
|
368
|
+
didForceFocusCallBack?(true)
|
|
369
|
+
|
|
251
370
|
let isActive = focusItemIsDescendant(nextFocusItem: context.nextFocusedItem)
|
|
252
371
|
activeStateNotifier.updateFocus(currentlyActive: isActive, context: context)
|
|
253
|
-
|
|
254
|
-
tryDidFocusCallCallback(context: context)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/// Try to send a callback in case focus manager request callback during force focus update
|
|
258
|
-
///
|
|
259
|
-
/// - Parameter context: An instance of UIFocusUpdateContext containing metadata of the focus related update.
|
|
260
|
-
private func tryDidFocusCallCallback(context: UIFocusUpdateContext) {
|
|
261
|
-
guard let callbackData = didFocusCallBack,
|
|
262
|
-
let fousedView = context.nextFocusedView as? FocusableView,
|
|
263
|
-
let itemId = fousedView.itemId
|
|
264
|
-
else {
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
let completion = callbackData.completion
|
|
269
|
-
let focusableItemId = callbackData.focusableItemId
|
|
270
|
-
if itemId == focusableItemId {
|
|
271
|
-
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
|
272
|
-
completion()
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
didFocusCallBack = nil
|
|
276
372
|
}
|
|
277
373
|
}
|
|
278
374
|
|
|
279
375
|
extension FocusableGroupView: FocusableGroupProtocol {
|
|
280
376
|
var canBecomeFocusable: Bool {
|
|
281
|
-
|
|
377
|
+
if let forceFocusEnabled = GroupProxyManager.shared.focusableGroupCanBecomeFocusableForceFocusUpdate(groupId: itemId) {
|
|
378
|
+
return forceFocusEnabled
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return !isFocusDisabled
|
|
282
382
|
}
|
|
283
383
|
}
|
|
@@ -35,6 +35,32 @@ public class FocusableView: ParallaxView {
|
|
|
35
35
|
"""
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
public private(set) var isPresented: Bool = false
|
|
39
|
+
@objc public var onRegistered: RCTDirectEventBlock?
|
|
40
|
+
|
|
41
|
+
func sendViewRegisteredEvent() {
|
|
42
|
+
var params: [String: Any] = [:]
|
|
43
|
+
params[GroupViewUpdateEvents.groupId] = groupId
|
|
44
|
+
params[GroupViewUpdateEvents.isFocusDisabled] = !canBecomeFocused
|
|
45
|
+
params[GroupViewUpdateEvents.itemId] = itemId
|
|
46
|
+
onRegistered?(params)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override public func didMoveToWindow() {
|
|
50
|
+
super.didMoveToWindow()
|
|
51
|
+
updateIsPresented()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private func updateIsPresented() {
|
|
55
|
+
isPresented =
|
|
56
|
+
window != nil &&
|
|
57
|
+
bounds.width > 0 &&
|
|
58
|
+
bounds.height > 0 &&
|
|
59
|
+
!isHidden &&
|
|
60
|
+
alpha > 0.01
|
|
61
|
+
registerView()
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
private weak var module: FocusableViewModule?
|
|
39
65
|
func setModule(_ module: FocusableViewModule) {
|
|
40
66
|
self.module = module
|
|
@@ -115,7 +141,7 @@ public class FocusableView: ParallaxView {
|
|
|
115
141
|
|
|
116
142
|
override public func layoutSubviews() {
|
|
117
143
|
super.layoutSubviews()
|
|
118
|
-
|
|
144
|
+
updateIsPresented()
|
|
119
145
|
if isFocusLayoutConfigured == false {
|
|
120
146
|
configureFocusLayout()
|
|
121
147
|
isFocusLayoutConfigured = true
|
|
@@ -125,6 +151,7 @@ public class FocusableView: ParallaxView {
|
|
|
125
151
|
private func registerView() {
|
|
126
152
|
guard let groupId,
|
|
127
153
|
itemId != nil,
|
|
154
|
+
isPresented,
|
|
128
155
|
isViewRegistered == false
|
|
129
156
|
else {
|
|
130
157
|
return
|
|
@@ -227,6 +254,10 @@ public class FocusableView: ParallaxView {
|
|
|
227
254
|
return focusable
|
|
228
255
|
}
|
|
229
256
|
|
|
257
|
+
if let forceFocusEnabled = GroupProxyManager.shared.focusableViewCanBecomeFocusableForceFocusUpdate(groupId: groupId, itemId: itemId) {
|
|
258
|
+
return forceFocusEnabled
|
|
259
|
+
}
|
|
260
|
+
|
|
230
261
|
return focusableGroup.canBecomeFocusable && focusable
|
|
231
262
|
}
|
|
232
263
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ContextResolverBridge.swift
|
|
3
|
+
// QuickBrickApple
|
|
4
|
+
//
|
|
5
|
+
// Created by Alex Zchut on 15/01/2026.
|
|
6
|
+
// Copyright © 2026 Applicaster LTD. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import React
|
|
11
|
+
import ZappCore
|
|
12
|
+
|
|
13
|
+
@objc(ContextResolverBridge)
|
|
14
|
+
class ContextResolverBridge: NSObject, RCTBridgeModule {
|
|
15
|
+
static func moduleName() -> String! {
|
|
16
|
+
"ContextResolverBridge"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class func requiresMainQueueSetup() -> Bool {
|
|
20
|
+
true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// prefered thread on which to run this native module
|
|
24
|
+
@objc var methodQueue: DispatchQueue {
|
|
25
|
+
DispatchQueue.main
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc func resolveContextKeys(_ keys: [String: Any],
|
|
29
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
30
|
+
rejecter _: @escaping RCTPromiseRejectBlock) {
|
|
31
|
+
var result: [String: Any] = [:]
|
|
32
|
+
|
|
33
|
+
for (key, value) in keys {
|
|
34
|
+
let isRequired = extractBoolValue(from: value)
|
|
35
|
+
let resolved = resolveStorage(key: key, required: isRequired)
|
|
36
|
+
result[key] = resolved ?? NSNull()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resolver(result)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func extractBoolValue(from value: Any) -> Bool {
|
|
43
|
+
if let number = value as? NSNumber {
|
|
44
|
+
number.boolValue
|
|
45
|
+
} else if let boolValue = value as? Bool {
|
|
46
|
+
boolValue
|
|
47
|
+
} else {
|
|
48
|
+
false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private func resolveStorage(key: String, required _: Bool) -> String? {
|
|
53
|
+
let data = key.split(separator: ".", maxSplits: 1)
|
|
54
|
+
let (namespace, storageKey): (String?, String) = {
|
|
55
|
+
switch data.count {
|
|
56
|
+
case 2:
|
|
57
|
+
let namespace = String(data.first ?? "")
|
|
58
|
+
let storageKey = String(data.last ?? "")
|
|
59
|
+
return (namespace.isEmpty ? nil : namespace, storageKey)
|
|
60
|
+
default:
|
|
61
|
+
return (nil, key)
|
|
62
|
+
}
|
|
63
|
+
}()
|
|
64
|
+
|
|
65
|
+
if let value = FacadeConnector.connector?.storage?.sessionStorageValue(for: storageKey,
|
|
66
|
+
namespace: namespace) {
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if let value = FacadeConnector.connector?.storage?.localStorageValue(for: storageKey,
|
|
71
|
+
namespace: namespace) {
|
|
72
|
+
return value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -30,7 +30,7 @@ RCT_EXTERN_METHOD(setAppLanguage:(NSString *)language
|
|
|
30
30
|
|
|
31
31
|
@interface RCT_EXTERN_MODULE (AnalyticsBridge, NSObject)
|
|
32
32
|
RCT_EXTERN_METHOD(postEvent:(NSString *)event
|
|
33
|
-
|
|
33
|
+
payload:(NSDictionary *)payload);
|
|
34
34
|
RCT_EXTERN_METHOD(postTimedEvent:(NSString *)event
|
|
35
35
|
payload:(NSDictionary *)payload);
|
|
36
36
|
RCT_EXTERN_METHOD(endTimedEvent:(NSString *)event
|
|
@@ -151,18 +151,24 @@ RCT_EXTERN_METHOD(switchLayout:(NSString *)layoutId
|
|
|
151
151
|
rejecter:(RCTPromiseRejectBlock)reject);
|
|
152
152
|
|
|
153
153
|
RCT_EXTERN_METHOD(getCurrentLayoutId:(RCTPromiseResolveBlock)resolve
|
|
154
|
-
|
|
154
|
+
rejecter:(RCTPromiseRejectBlock)reject);
|
|
155
155
|
|
|
156
156
|
RCT_EXTERN_METHOD(getDefaultLayoutId:(RCTPromiseResolveBlock)resolve
|
|
157
|
-
|
|
157
|
+
rejecter:(RCTPromiseRejectBlock)reject);
|
|
158
158
|
@end
|
|
159
159
|
|
|
160
|
-
@interface RCT_EXTERN_MODULE(AirPlayButtonModule, RCTViewManager)
|
|
160
|
+
@interface RCT_EXTERN_MODULE (AirPlayButtonModule, RCTViewManager)
|
|
161
161
|
RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor);
|
|
162
162
|
RCT_EXPORT_VIEW_PROPERTY(activeTintColor, UIColor);
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
@end
|
|
165
165
|
|
|
166
166
|
@interface RCT_EXTERN_MODULE (ReactNativeEventBusBridge, NSObject)
|
|
167
167
|
RCT_EXTERN_METHOD(postEvent:(NSDictionary *)event);
|
|
168
168
|
@end
|
|
169
|
+
|
|
170
|
+
@interface RCT_EXTERN_MODULE (ContextResolverBridge, NSObject)
|
|
171
|
+
RCT_EXTERN_METHOD(resolveContextKeys:(NSDictionary *)keys
|
|
172
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
173
|
+
rejecter:(RCTPromiseRejectBlock)rejecter);
|
|
174
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/quick-brick-native-apple",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.18.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"
|