@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "QuickBrickApple",
3
- "version": "6.13.0",
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.13.0"
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
- public func registerGroup(_ group: FocusableGroupView) async -> Bool {
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
- public func unregisterGroup(withId groupId: String) async {
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
- public func getGroup(by groupID: String) async -> FocusableGroupView? {
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
- public func registerItem(_ item: FocusableView) async -> Bool {
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
- public func unregisterItem(withId itemId: String, inGroup groupId: String) async {
75
+ func unregisterItem(withId itemId: String, inGroup groupId: String) async {
74
76
  await itemStorage.unregisterItem(withId: itemId, inGroup: groupId)
75
77
  }
76
78
 
77
- public func getItems(forGroup groupId: String) async -> [String: FocusableView] {
79
+ func getItems(forGroup groupId: String) async -> [String: FocusableView] {
78
80
  await itemStorage.items(forGroup: groupId)
79
81
  }
80
82
 
81
- public func getItem(withId itemId: String, inGroup groupId: String) async -> FocusableView? {
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
- public func notifyFocusableItemDidUpdatePreferredFocus(focusableView: FocusableView) {
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
- public func updateFocus(groupId: String?,
97
- itemId: String?,
98
- needsForceUpdate: Bool = false,
99
- completion: ((Bool) -> Void)? = nil) {
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
- public var bridge: RCTBridge?
21
+ var bridge: RCTBridge?
22
22
 
23
23
  static func moduleName() -> String! {
24
24
  kFocusableManagerModule
25
25
  }
26
26
 
27
- public class func requiresMainQueueSetup() -> Bool {
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 public var methodQueue: DispatchQueue {
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
- public func sendFocusableGroupRegisteredUpdate(_ focusableGroupView: FocusableGroupView) {
19
+ func sendFocusableGroupRegisteredUpdate(_ focusableGroupView: FocusableGroupView) {
20
20
  focusableGroupRegistrationUpdates.send(focusableGroupView)
21
21
  }
22
22
 
23
- public func sendFocusableItemDidUpdatePreferredFocus(_ focusableView: FocusableView) {
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: focusGuide.layoutFrame)
13
- backgroundColor = UIColor.green.withAlphaComponent(0.15)
14
- layer.borderColor = UIColor.green.withAlphaComponent(0.3).cgColor
15
- layer.borderWidth = 1
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
- nil
37
+ fatalError("init(coder:) has not been implemented")
20
38
  }
21
39
  }
22
40
 
@@ -226,7 +226,6 @@ public class FocusableView: ParallaxView {
226
226
  }
227
227
 
228
228
  DispatchQueue.main.async { [weak self] in
229
-
230
229
  FocusableGroupManager.shared.updateFocus(groupId: self?.groupId,
231
230
  itemId: self?.itemId,
232
231
  needsForceUpdate: true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-native-apple",
3
- "version": "6.13.0",
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"