@applicaster/quick-brick-native-apple 6.13.0 → 6.14.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/FocusableGroupManager+MovementDidFailNotification.swift +181 -0
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableGroupManager.swift +14 -12
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableManagerModule.swift +3 -3
- package/apple/tvos/Helpers/FocusableGroupManager/Protocols/UpdateService.swift +2 -2
- package/apple/tvos/Views/FocusableView/FocusableView+FocusGuide.swift +24 -6
- package/apple/tvos/Views/FocusableView/FocusableView.swift +0 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "QuickBrickApple",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.14.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.14.0"
|
|
20
20
|
},
|
|
21
21
|
"requires_arc": true,
|
|
22
22
|
"source_files": "universal/**/*.{m,swift}",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//
|
|
2
|
+
// FocusableGroupManager+MovementDidFailNotification.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Anton Kononenko on 9/12/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import React
|
|
9
|
+
import UIKit
|
|
10
|
+
|
|
11
|
+
extension FocusableGroupManager {
|
|
12
|
+
public func registerForMovementDidFailNotification() {
|
|
13
|
+
NotificationCenter.default.addObserver(
|
|
14
|
+
forName: UIFocusSystem.movementDidFailNotification,
|
|
15
|
+
object: nil,
|
|
16
|
+
queue: .main
|
|
17
|
+
) { n in
|
|
18
|
+
guard let rootViewController = UIApplication.shared.rootViewController(),
|
|
19
|
+
let rootView = rootViewController.view as? RCTRootView,
|
|
20
|
+
let userInfo = n.userInfo,
|
|
21
|
+
let ctx = userInfo[UIFocusSystem.focusUpdateContextUserInfoKey] as? UIFocusUpdateContext else { return }
|
|
22
|
+
let prev = ctx.previouslyFocusedItem as? UIView
|
|
23
|
+
let next = ctx.nextFocusedItem as? UIView
|
|
24
|
+
|
|
25
|
+
Task { @MainActor
|
|
26
|
+
[weak self] in
|
|
27
|
+
guard let self else { return }
|
|
28
|
+
logFocusError(userInfo: userInfo,
|
|
29
|
+
ctx: ctx,
|
|
30
|
+
prev: prev,
|
|
31
|
+
next: next)
|
|
32
|
+
|
|
33
|
+
// TODO: Unocomment for debugging
|
|
34
|
+
// simulateFocusUpdateRequest(prev: prev, next: next)
|
|
35
|
+
|
|
36
|
+
await tryToRestoreCorruptedFocusState(rootView: rootView, next: next)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@MainActor
|
|
42
|
+
private func tryToRestoreCorruptedFocusState(rootView: RCTRootView, next: UIView?) async {
|
|
43
|
+
guard let next else { return }
|
|
44
|
+
|
|
45
|
+
rootView.reactPreferredFocusEnvironments = [next]
|
|
46
|
+
|
|
47
|
+
rootView.setNeedsFocusUpdate()
|
|
48
|
+
rootView.updateFocusIfNeeded()
|
|
49
|
+
|
|
50
|
+
DispatchQueue.main.async {
|
|
51
|
+
rootView.reactPreferredFocusEnvironments = []
|
|
52
|
+
rootView.reactPreferredFocusedView = nil
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func simulateFocusUpdateRequest(prev: UIView?, next: UIView?) {
|
|
57
|
+
guard let prev,
|
|
58
|
+
let next else { return }
|
|
59
|
+
let container = lowestCommonAncestor(prev, next)
|
|
60
|
+
?? next.window?.rootViewController?.view
|
|
61
|
+
?? prev
|
|
62
|
+
|
|
63
|
+
let guide = UIFocusGuide()
|
|
64
|
+
|
|
65
|
+
container.addLayoutGuide(guide)
|
|
66
|
+
NSLayoutConstraint.activate([
|
|
67
|
+
guide.topAnchor.constraint(equalTo: container.topAnchor),
|
|
68
|
+
guide.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
69
|
+
guide.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
70
|
+
guide.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
71
|
+
])
|
|
72
|
+
guide.preferredFocusEnvironments = [next]
|
|
73
|
+
|
|
74
|
+
let sim = UIFocusDebugger.simulateFocusUpdateRequest(from: container)
|
|
75
|
+
print("⚠️ Simulation result:\n\(sim)")
|
|
76
|
+
|
|
77
|
+
print("⚠️ Focus groups (next):\n\(UIFocusDebugger.focusGroups(for: next))")
|
|
78
|
+
print("⚠️ Preferred chain (next):\n\(UIFocusDebugger.preferredFocusEnvironments(for: next))")
|
|
79
|
+
|
|
80
|
+
container.removeLayoutGuide(guide)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private func logFocusError(userInfo _: [AnyHashable: Any],
|
|
84
|
+
ctx: UIFocusUpdateContext,
|
|
85
|
+
prev: UIView?,
|
|
86
|
+
next: UIView?) {
|
|
87
|
+
print("⚠️ Focus movement FAILED. heading=\(ctx.focusHeading.rawValue)")
|
|
88
|
+
print(" prev=\(String(describing: prev))")
|
|
89
|
+
print(" next=\(String(describing: next))")
|
|
90
|
+
|
|
91
|
+
if let next {
|
|
92
|
+
print(" ▶︎ UIFocusDebugger.checkFocusability(next):")
|
|
93
|
+
print(UIFocusDebugger.checkFocusability(for: next))
|
|
94
|
+
|
|
95
|
+
let g = rectOnScreen(next)
|
|
96
|
+
print(" ▶︎ Geometry next: inWindow=\(String(describing: g.inWindow)) window=\(String(describing: g.windowBounds)) onScreen=\(g.onScreen)")
|
|
97
|
+
|
|
98
|
+
print(" ▶︎ Focus chain (next): \(focusPath(next))")
|
|
99
|
+
}
|
|
100
|
+
if let prev {
|
|
101
|
+
let g = rectOnScreen(prev)
|
|
102
|
+
print(" ▶︎ Geometry prev: inWindow=\(String(describing: g.inWindow)) onScreen=\(g.onScreen)")
|
|
103
|
+
print(" ▶︎ Focus chain (prev): \(focusPath(prev))")
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// MARK: - Geometry
|
|
108
|
+
|
|
109
|
+
/// Returns the view's rect in window coords, the window bounds, and whether it's (even partially) on-screen.
|
|
110
|
+
func rectOnScreen(_ view: UIView) -> (inWindow: CGRect?, windowBounds: CGRect?, onScreen: Bool) {
|
|
111
|
+
guard let win = view.window else {
|
|
112
|
+
return (nil, nil, false)
|
|
113
|
+
}
|
|
114
|
+
let inWin = view.convert(view.bounds, to: win)
|
|
115
|
+
let on = inWin.intersects(win.bounds) && !inWin.isEmpty
|
|
116
|
+
return (inWin, win.bounds, on)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - Focus path
|
|
120
|
+
|
|
121
|
+
func focusPath(_ env: UIFocusEnvironment) -> String {
|
|
122
|
+
var names: [String] = []
|
|
123
|
+
var cursor: UIFocusEnvironment? = env
|
|
124
|
+
|
|
125
|
+
func name(_ obj: AnyObject) -> String {
|
|
126
|
+
let cls = String(describing: type(of: obj))
|
|
127
|
+
if obj is UIView {
|
|
128
|
+
return "\(cls)"
|
|
129
|
+
} else if obj is UIViewController {
|
|
130
|
+
return "\(cls)"
|
|
131
|
+
} else {
|
|
132
|
+
return cls
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
while let e = cursor as AnyObject? {
|
|
137
|
+
names.append(name(e))
|
|
138
|
+
if let v = e as? UIView {
|
|
139
|
+
cursor = v.superview ?? v.next as? UIFocusEnvironment ?? v.parentFocusEnvironment
|
|
140
|
+
} else if let vc = e as? UIViewController {
|
|
141
|
+
cursor = vc.parent ?? vc.view?.superview ?? vc.parentFocusEnvironment
|
|
142
|
+
} else {
|
|
143
|
+
cursor = (e as? UIFocusEnvironment)?.parentFocusEnvironment
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if e is UIApplication { break }
|
|
147
|
+
if String(describing: type(of: e)) == "AppDelegate" { break }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return names.joined(separator: " → ")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func lowestCommonAncestor(_ a: UIView, _ b: UIView) -> UIView? {
|
|
154
|
+
var seen = Set<ObjectIdentifier>()
|
|
155
|
+
var x: UIView? = a
|
|
156
|
+
while let v = x {
|
|
157
|
+
seen.insert(ObjectIdentifier(v)); x = v.superview
|
|
158
|
+
}
|
|
159
|
+
var y: UIView? = b
|
|
160
|
+
while let v = y {
|
|
161
|
+
if seen.contains(ObjectIdentifier(v)) { return v }
|
|
162
|
+
y = v.superview
|
|
163
|
+
}
|
|
164
|
+
return nil
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@inline(__always)
|
|
168
|
+
func currentFocusedItem(relativeTo view: UIView?) -> UIFocusItem? {
|
|
169
|
+
if #available(tvOS 15.0, *) {
|
|
170
|
+
if let fs = view?.window?.windowScene?.focusSystem { return fs.focusedItem }
|
|
171
|
+
// Fallback if you don't have a view handy:
|
|
172
|
+
for scene in UIApplication.shared.connectedScenes {
|
|
173
|
+
if let ws = scene as? UIWindowScene { return ws.focusSystem?.focusedItem }
|
|
174
|
+
}
|
|
175
|
+
return nil
|
|
176
|
+
} else {
|
|
177
|
+
// tvOS < 15
|
|
178
|
+
return UIScreen.main.focusedItem
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -29,11 +29,13 @@ class FocusableGroupManager {
|
|
|
29
29
|
self.groupStorage = groupStorage
|
|
30
30
|
self.itemStorage = itemStorage
|
|
31
31
|
self.updateService = updateService
|
|
32
|
+
|
|
33
|
+
registerForMovementDidFailNotification()
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
// MARK: - Group Managment
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
func registerGroup(_ group: FocusableGroupView) async -> Bool {
|
|
37
39
|
let wasRegistered = await groupStorage.registerGroup(group)
|
|
38
40
|
guard wasRegistered else {
|
|
39
41
|
return false
|
|
@@ -43,7 +45,7 @@ class FocusableGroupManager {
|
|
|
43
45
|
return true
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
func unregisterGroup(withId groupId: String) async {
|
|
47
49
|
await groupStorage.unregisterGroup(withId: groupId)
|
|
48
50
|
let items = await itemStorage.items(forGroup: groupId)
|
|
49
51
|
for itemId in items.keys {
|
|
@@ -51,13 +53,13 @@ class FocusableGroupManager {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
func getGroup(by groupID: String) async -> FocusableGroupView? {
|
|
55
57
|
await groupStorage.group(by: groupID)
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// MARK: - Item Management
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
func registerItem(_ item: FocusableView) async -> Bool {
|
|
61
63
|
let success = await itemStorage.registerItem(item)
|
|
62
64
|
if success,
|
|
63
65
|
let groupId = await item.groupId {
|
|
@@ -70,19 +72,19 @@ class FocusableGroupManager {
|
|
|
70
72
|
return success
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
func unregisterItem(withId itemId: String, inGroup groupId: String) async {
|
|
74
76
|
await itemStorage.unregisterItem(withId: itemId, inGroup: groupId)
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
func getItems(forGroup groupId: String) async -> [String: FocusableView] {
|
|
78
80
|
await itemStorage.items(forGroup: groupId)
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
func getItem(withId itemId: String, inGroup groupId: String) async -> FocusableView? {
|
|
82
84
|
await itemStorage.item(withId: itemId, inGroup: groupId)
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
func notifyFocusableItemDidUpdatePreferredFocus(focusableView: FocusableView) {
|
|
86
88
|
updateService.sendFocusableItemDidUpdatePreferredFocus(focusableView)
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -93,10 +95,10 @@ class FocusableGroupManager {
|
|
|
93
95
|
/// - itemId: Id of the focusable item
|
|
94
96
|
/// - needsForceUpdate: if value is true after make item as preferred item focus, It will also request Focus Engine to focus immidiately
|
|
95
97
|
/// - completion: completion block in case need, that will be called when focusable item did focus
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
func updateFocus(groupId: String?,
|
|
99
|
+
itemId: String?,
|
|
100
|
+
needsForceUpdate: Bool = false,
|
|
101
|
+
completion: ((Bool) -> Void)? = nil) {
|
|
100
102
|
Task { @MainActor in
|
|
101
103
|
guard
|
|
102
104
|
let groupId,
|
|
@@ -18,18 +18,18 @@ class FocusableManagerModule: NSObject, RCTBridgeModule {
|
|
|
18
18
|
var delayTimer = 100
|
|
19
19
|
|
|
20
20
|
/// main React bridge
|
|
21
|
-
|
|
21
|
+
var bridge: RCTBridge?
|
|
22
22
|
|
|
23
23
|
static func moduleName() -> String! {
|
|
24
24
|
kFocusableManagerModule
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
class func requiresMainQueueSetup() -> Bool {
|
|
28
28
|
true
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/// prefered thread on which to run this native module
|
|
32
|
-
@objc
|
|
32
|
+
@objc var methodQueue: DispatchQueue {
|
|
33
33
|
DispatchQueue.main
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -16,11 +16,11 @@ class UpdateService {
|
|
|
16
16
|
let focusableGroupRegistrationUpdates = PassthroughSubject<FocusableGroupView, Never>()
|
|
17
17
|
let focusableItemDidUpdatePreferredFocus = PassthroughSubject<FocusableView, Never>()
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
func sendFocusableGroupRegisteredUpdate(_ focusableGroupView: FocusableGroupView) {
|
|
20
20
|
focusableGroupRegistrationUpdates.send(focusableGroupView)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
func sendFocusableItemDidUpdatePreferredFocus(_ focusableView: FocusableView) {
|
|
24
24
|
focusableItemDidUpdatePreferredFocus.send(focusableView)
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -7,16 +7,34 @@
|
|
|
7
7
|
|
|
8
8
|
import Foundation
|
|
9
9
|
|
|
10
|
-
class FocusGuideDebugView: UIView {
|
|
10
|
+
final class FocusGuideDebugView: UIView {
|
|
11
|
+
private weak var guide: UIFocusGuide?
|
|
12
|
+
|
|
11
13
|
init(focusGuide: UIFocusGuide) {
|
|
12
|
-
super.init(frame:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
super.init(frame: .zero)
|
|
15
|
+
guide = focusGuide
|
|
16
|
+
|
|
17
|
+
backgroundColor = UIColor.red.withAlphaComponent(0.15)
|
|
18
|
+
layer.borderColor = UIColor.red.withAlphaComponent(1).cgColor
|
|
19
|
+
layer.borderWidth = 20
|
|
20
|
+
|
|
21
|
+
translatesAutoresizingMaskIntoConstraints = false
|
|
22
|
+
|
|
23
|
+
if let container = focusGuide.owningView {
|
|
24
|
+
container.addSubview(self)
|
|
25
|
+
|
|
26
|
+
NSLayoutConstraint.activate([
|
|
27
|
+
leftAnchor.constraint(equalTo: focusGuide.leftAnchor),
|
|
28
|
+
rightAnchor.constraint(equalTo: focusGuide.rightAnchor),
|
|
29
|
+
topAnchor.constraint(equalTo: focusGuide.topAnchor),
|
|
30
|
+
bottomAnchor.constraint(equalTo: focusGuide.bottomAnchor),
|
|
31
|
+
])
|
|
32
|
+
}
|
|
16
33
|
}
|
|
17
34
|
|
|
35
|
+
@available(*, unavailable)
|
|
18
36
|
required init?(coder _: NSCoder) {
|
|
19
|
-
|
|
37
|
+
fatalError("init(coder:) has not been implemented")
|
|
20
38
|
}
|
|
21
39
|
}
|
|
22
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/quick-brick-native-apple",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.14.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"
|