@applicaster/quick-brick-native-apple 6.15.2 → 6.16.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.15.2",
3
+ "version": "6.16.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.15.2"
19
+ "tag": "@@applicaster/quick-brick-native-apple/6.16.0"
20
20
  },
21
21
  "requires_arc": true,
22
22
  "source_files": "universal/**/*.{m,swift}",
@@ -44,8 +44,8 @@ class FocusableManagerModule: NSObject, RCTBridgeModule {
44
44
  let delay = DispatchTime.now() + DispatchTimeInterval.milliseconds(delayTimer)
45
45
 
46
46
  DispatchQueue.main.asyncAfter(deadline: delay) {
47
- FocusableGroupManager.shared.updateFocus(groupId: groupId,
48
- itemId: itemId)
47
+ GroupProxyManager.shared.updateFocus(groupId: groupId,
48
+ itemId: itemId)
49
49
  }
50
50
  }
51
51
 
@@ -60,13 +60,13 @@ class FocusableManagerModule: NSObject, RCTBridgeModule {
60
60
  let delay = DispatchTime.now() + DispatchTimeInterval.milliseconds(delayTimer)
61
61
 
62
62
  DispatchQueue.main.asyncAfter(deadline: delay) {
63
- FocusableGroupManager.shared.updateFocus(groupId: groupId,
64
- itemId: itemId,
65
- needsForceUpdate: true,
66
- completion: { succeed in
67
- callback?([succeed])
63
+ GroupProxyManager.shared.updateFocus(groupId: groupId,
64
+ itemId: itemId,
65
+ needsForceUpdate: true,
66
+ completion: { succeed in
67
+ callback?([succeed])
68
68
 
69
- })
69
+ })
70
70
  }
71
71
  }
72
72
  }
@@ -0,0 +1,128 @@
1
+ //
2
+ // GroupProxy.swift
3
+ // Pods
4
+ //
5
+ // Created by Anton Kononenko on 12/19/24.
6
+ //
7
+
8
+ import Foundation
9
+ import XrayLogger
10
+
11
+ class GroupProxy {
12
+ let groupId: String
13
+ private weak var group: FocusableGroupView?
14
+ private var children: [WeakBox<FocusableView>] = []
15
+ private let lock = NSLock()
16
+
17
+ var delayedPreferredFocus: FocusableView?
18
+
19
+ private lazy var logger = Logger.getLogger(for: "QuickBrickApple/FocusableGroupManager/GroupProxy")
20
+
21
+ init(groupId: String) {
22
+ self.groupId = groupId
23
+ }
24
+
25
+ // MARK: - Child Management
26
+
27
+ func addChild(_ view: FocusableView) {
28
+ lock.lock()
29
+ children.append(WeakBox(view))
30
+ lock.unlock()
31
+ }
32
+
33
+ func removeChild(_ view: FocusableView) {
34
+ lock.lock()
35
+ defer { lock.unlock() }
36
+
37
+ children.removeAll { $0.value === view }
38
+
39
+ if children.isEmpty, group == nil {
40
+ GroupProxyManager.shared.removeProxy(forGroupId: groupId)
41
+ }
42
+
43
+ if delayedPreferredFocus === view {
44
+ delayedPreferredFocus = nil
45
+ }
46
+ }
47
+
48
+ // MARK: - Group Binding
49
+
50
+ @MainActor func bindGroup(_ group: FocusableGroupView) {
51
+ // Check for duplicate group binding
52
+ if let existingGroup = self.group {
53
+ logger?.warningLog(message: """
54
+ ⚠️ Duplicate group binding detected for groupId: '\(groupId)'
55
+ Previous group: \(existingGroup.debugDescription)
56
+ New group: \(group.debugDescription)
57
+ Overwriting previous binding.
58
+ """)
59
+
60
+ #if DEBUG
61
+ // assertionFailure("Duplicate group binding for groupId '\(groupId)'. Check React Native component IDs.")
62
+ #endif
63
+ }
64
+
65
+ self.group = group
66
+
67
+ if let preferredFocusView = delayedPreferredFocus {
68
+ applyPreferredFocus(group,
69
+ view: preferredFocusView)
70
+ delayedPreferredFocus = nil
71
+ }
72
+ }
73
+
74
+ func unbindGroup() {
75
+ lock.lock()
76
+ defer { lock.unlock() }
77
+
78
+ if children.isEmpty {
79
+ GroupProxyManager.shared.removeProxy(forGroupId: groupId)
80
+ }
81
+
82
+ group = nil
83
+ }
84
+
85
+ // MARK: - Preferred Focus
86
+
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)
93
+ }
94
+ }
95
+
96
+ @MainActor func updatePreferredFocus(_ view: FocusableView) {
97
+ lock.lock()
98
+ let currentGroup = group
99
+ lock.unlock()
100
+
101
+ if let currentGroup {
102
+ applyPreferredFocus(currentGroup,
103
+ view: view)
104
+ } else {
105
+ if view.preferredFocus {
106
+ delayedPreferredFocus = view
107
+ }
108
+ }
109
+ }
110
+
111
+ /// Get the bound group (if any)
112
+ func getGroup() -> FocusableGroupView? {
113
+ lock.lock()
114
+ defer { lock.unlock() }
115
+
116
+ return group
117
+ }
118
+ }
119
+
120
+ // MARK: - WeakBox Helper
121
+
122
+ private class WeakBox<T: AnyObject> {
123
+ weak var value: T?
124
+
125
+ init(_ value: T) {
126
+ self.value = value
127
+ }
128
+ }
@@ -0,0 +1,101 @@
1
+ //
2
+ // GroupProxyManager.swift
3
+ // Pods
4
+ //
5
+ // Created by Anton Kononenko on 12/19/24.
6
+ //
7
+
8
+ import Foundation
9
+ import React
10
+
11
+ class GroupProxyManager {
12
+ static let shared = GroupProxyManager()
13
+
14
+ private let lock = NSLock()
15
+ private var proxies: [GroupProxy] = []
16
+
17
+ private init() {}
18
+
19
+ // MARK: - Proxy Access
20
+
21
+ func getOrCreateProxy(forGroupId groupId: String) -> GroupProxy {
22
+ lock.lock()
23
+ defer { lock.unlock() }
24
+
25
+ if let existing = proxies.first(where: { $0.groupId == groupId }) {
26
+ return existing
27
+ }
28
+
29
+ let proxy = GroupProxy(groupId: groupId)
30
+ proxies.append(proxy)
31
+ return proxy
32
+ }
33
+
34
+ // MARK: - Cleanup
35
+
36
+ func removeProxy(forGroupId groupId: String) {
37
+ lock.lock()
38
+ defer { lock.unlock() }
39
+
40
+ proxies.removeAll { $0.groupId == groupId }
41
+ }
42
+
43
+ // MARK: - Focus Update
44
+
45
+ func updateFocus(groupId: String?,
46
+ itemId: String?,
47
+ needsForceUpdate: Bool = false,
48
+ completion: ((Bool) -> Void)? = nil) {
49
+ guard let groupId,
50
+ let itemId else {
51
+ completion?(false)
52
+ return
53
+ }
54
+
55
+ lock.lock()
56
+ let proxy = proxies.first(where: { $0.groupId == groupId })
57
+ lock.unlock()
58
+
59
+ guard let proxy,
60
+ let groupView = proxy.getGroup() else {
61
+ completion?(false)
62
+ return
63
+ }
64
+
65
+ DispatchQueue.main.async {
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
+ }
100
+ }
101
+ }
@@ -6,13 +6,10 @@
6
6
  // Copyright © 2019 Kononenko. All rights reserved.
7
7
  //
8
8
 
9
- import Combine
10
9
  import Foundation
11
10
  import React
12
11
  import UIKit
13
12
 
14
- /// Focusable Group View that implements UIFocusGuide instance that catches focus event
15
- ///
16
13
  public class FocusableGroupView: RCTTVView {
17
14
  override public var debugDescription: String {
18
15
  let groupIdDesc = groupId ?? "NO_GROUP"
@@ -35,9 +32,6 @@ public class FocusableGroupView: RCTTVView {
35
32
  """
36
33
  }
37
34
 
38
- private var cancellables = Set<AnyCancellable>()
39
-
40
- /// Completion that will be used when focus manager forcing to update focusable group
41
35
  public var didFocusCallBack: (completion: () -> Void, focusableItemId: String)?
42
36
 
43
37
  private lazy var activeStateNotifier: FocusableGroupStateNotifier = FocusableGroupStateNotifierDefault(action: notifyReactNativeFocusUpdate)
@@ -45,40 +39,38 @@ public class FocusableGroupView: RCTTVView {
45
39
  @objc public var onGroupFocus: RCTDirectEventBlock?
46
40
  @objc public var onGroupBlur: RCTDirectEventBlock?
47
41
 
48
- /// Define if focus enabled for current view
49
42
  @objc public var isFocusDisabled: Bool = false
50
43
 
51
- /// Current group will try to find initial index id in groups
52
44
  @objc public var dependantGroupIds: [String]?
53
45
 
54
- /// Check if preferred focus environment disabled
55
46
  @objc public var isWithMemory: Bool = true
56
47
  var isPreferredFocusDisabled: Bool {
57
48
  isWithMemory == false
58
49
  }
59
50
 
60
- /// ID of the Connected GroupView provided by React-Native env
61
51
  @objc public var itemId: String? {
62
52
  didSet {
63
53
  if itemId != oldValue {
64
- Task(priority: .userInitiated) {
65
- await manager.registerGroup(self)
54
+ if oldValue != nil {
55
+ proxy?.unbindGroup()
56
+ proxy = nil
57
+ }
58
+
59
+ if let newId = itemId {
60
+ proxy = GroupProxyManager.shared.getOrCreateProxy(forGroupId: newId)
61
+ proxy?.bindGroup(self)
66
62
  }
67
63
  }
68
64
  }
69
65
  }
70
66
 
71
- /// ID of the parent group, if relevant
72
67
  @objc public var groupId: String?
73
68
 
74
69
  @MainActor private var userPreferredFocusEnvironments: [UIFocusEnvironment]?
75
70
  @MainActor private var customPreferredFocusEnvironment: [UIFocusEnvironment]?
76
71
 
77
- private var manager: FocusableGroupManager {
78
- FocusableGroupManager.shared
79
- }
72
+ private var proxy: GroupProxy?
80
73
 
81
- /// Manager that connects View instance to FocusableGroupViewModule
82
74
  private weak var module: FocusableGroupViewModule?
83
75
  public func setModule(_ module: FocusableGroupViewModule) {
84
76
  self.module = module
@@ -90,17 +82,20 @@ public class FocusableGroupView: RCTTVView {
90
82
 
91
83
  override public func removeFromSuperview() {
92
84
  super.removeFromSuperview()
85
+ cleanup()
86
+ }
87
+
88
+ deinit {
89
+ cleanup()
90
+ }
91
+
92
+ private func cleanup() {
93
93
  guard let itemId else {
94
94
  return
95
95
  }
96
96
 
97
- Task {
98
- // Removing when react native releases the view
99
- await manager.unregisterGroup(withId: itemId)
100
-
101
- cancellables.forEach { $0.cancel() }
102
- cancellables.removeAll()
103
- }
97
+ proxy?.unbindGroup()
98
+ proxy = nil
104
99
  }
105
100
 
106
101
  public required init?(coder aDecoder: NSCoder) {
@@ -120,6 +115,14 @@ public class FocusableGroupView: RCTTVView {
120
115
  customPreferredFocusEnvironment = [view]
121
116
  }
122
117
 
118
+ @MainActor func setUserPreferredFocusEnvironments(_ environments: [UIFocusEnvironment]?) {
119
+ userPreferredFocusEnvironments = environments
120
+ }
121
+
122
+ @MainActor func getUserPreferredFocusEnvironments() -> [UIFocusEnvironment]? {
123
+ userPreferredFocusEnvironments
124
+ }
125
+
123
126
  let groupFocusGuide = UIFocusGuide()
124
127
 
125
128
  private func setupFocus() {
@@ -129,21 +132,6 @@ public class FocusableGroupView: RCTTVView {
129
132
  groupFocusGuide.topAnchor.constraint(equalTo: topAnchor).isActive = true
130
133
  groupFocusGuide.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
131
134
  groupFocusGuide.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
132
-
133
- manager.focusableItemDidUpdatePreferredFocus
134
- .filter { self.groupId != nil && $0.groupId == self.itemId }
135
- .sink { [weak self] focusableView in
136
- guard let self else { return }
137
- guard focusableView.preferredFocus == false else {
138
- userPreferredFocusEnvironments = [focusableView]
139
- return
140
- }
141
-
142
- if let currentPreferred = userPreferredFocusEnvironments?.first as? FocusableView,
143
- currentPreferred == focusableView {
144
- userPreferredFocusEnvironments = nil
145
- }
146
- }.store(in: &cancellables)
147
135
  }
148
136
 
149
137
  private func createFocusEventParams(context: UIFocusUpdateContext, isActive: Bool) -> [String: Any] {
@@ -6,7 +6,6 @@
6
6
  // Copyright © 2019 Anton Kononenko. All rights reserved.
7
7
  //
8
8
 
9
- import Combine
10
9
  import Foundation
11
10
  import React
12
11
  import UIKit
@@ -36,8 +35,6 @@ public class FocusableView: ParallaxView {
36
35
  """
37
36
  }
38
37
 
39
- private var cancellables = Set<AnyCancellable>()
40
-
41
38
  private weak var module: FocusableViewModule?
42
39
  func setModule(_ module: FocusableViewModule) {
43
40
  self.module = module
@@ -58,28 +55,21 @@ public class FocusableView: ParallaxView {
58
55
  }
59
56
  }
60
57
 
61
- /// Define if view can become focused
62
58
  @objc open var focusable = true
63
59
 
64
- private var manager: FocusableGroupManager {
65
- FocusableGroupManager.shared
66
- }
67
-
68
- @MainActor private weak var focusableGroup: FocusableGroupProtocol? {
69
- didSet {
70
- manager.notifyFocusableItemDidUpdatePreferredFocus(focusableView: self)
71
- }
72
- }
60
+ private var proxy: GroupProxy?
73
61
 
74
62
  @MainActor @objc public var preferredFocus: Bool = false {
75
63
  didSet {
76
- manager.notifyFocusableItemDidUpdatePreferredFocus(focusableView: self)
64
+ if oldValue == preferredFocus {
65
+ return
66
+ }
67
+
68
+ proxy?.updatePreferredFocus(self)
77
69
  }
78
70
  }
79
71
 
80
72
  private var isFocusLayoutConfigured = false
81
-
82
- /// Define if view was registered
83
73
  private var isViewRegistered: Bool = false
84
74
 
85
75
  override public init(frame: CGRect) {
@@ -92,31 +82,34 @@ public class FocusableView: ParallaxView {
92
82
  initialize()
93
83
  }
94
84
 
95
- /// Initialize component
96
85
  private func initialize() {
97
86
  delegate = self
98
-
99
- manager.focusableGroupRegistrationUpdates
100
- .filter { $0.itemId == self.groupId }
101
- .sink { [weak self] focusableGroup in
102
- guard let self else { return }
103
- DispatchQueue.main.async {
104
- self.focusableGroup = focusableGroup
105
- }
106
- }.store(in: &cancellables)
107
87
  }
108
88
 
109
- /// ID of the View provided by React-Native env
110
89
  @objc private(set) var itemId: String? {
111
90
  didSet {
112
- registerView()
91
+ if itemId != oldValue {
92
+ if oldValue != nil {
93
+ proxy?.removeChild(self)
94
+ isViewRegistered = false
95
+ }
96
+
97
+ registerView()
98
+ }
113
99
  }
114
100
  }
115
101
 
116
- /// ID of the View provided by React-Native env
117
102
  @objc private(set) var groupId: String? {
118
103
  didSet {
119
- registerView()
104
+ if groupId != oldValue {
105
+ if oldValue != nil {
106
+ proxy?.removeChild(self)
107
+ proxy = nil
108
+ isViewRegistered = false
109
+ }
110
+
111
+ registerView()
112
+ }
120
113
  }
121
114
  }
122
115
 
@@ -129,35 +122,31 @@ public class FocusableView: ParallaxView {
129
122
  }
130
123
  }
131
124
 
132
- /// Register View in FocusableGroupManager
133
125
  private func registerView() {
134
- Task(priority: .userInitiated) {
135
- guard itemId != nil,
136
- groupId != nil,
137
- isViewRegistered == false,
138
- await FocusableGroupManager.shared.registerItem(self)
139
- else {
140
- return
141
- }
142
-
143
- await MainActor.run {
144
- isViewRegistered = true
145
- }
126
+ guard let groupId,
127
+ itemId != nil,
128
+ isViewRegistered == false
129
+ else {
130
+ return
146
131
  }
132
+
133
+ proxy = GroupProxyManager.shared.getOrCreateProxy(forGroupId: groupId)
134
+ proxy?.addChild(self)
135
+ isViewRegistered = true
147
136
  }
148
137
 
149
138
  override public func removeFromSuperview() {
150
139
  super.removeFromSuperview()
151
- guard let itemId, let groupId else {
152
- return
153
- }
140
+ cleanup()
141
+ }
154
142
 
155
- Task {
156
- await FocusableGroupManager.shared.unregisterItem(withId: itemId, inGroup: groupId)
157
- }
143
+ deinit {
144
+ cleanup()
145
+ }
158
146
 
159
- cancellables.forEach { $0.cancel() }
160
- cancellables.removeAll()
147
+ private func cleanup() {
148
+ proxy?.removeChild(self)
149
+ proxy = nil
161
150
  }
162
151
 
163
152
  private func addFocusGuideIfNeeded(tag: NSNumber?,
@@ -226,32 +215,18 @@ public class FocusableView: ParallaxView {
226
215
  }
227
216
 
228
217
  DispatchQueue.main.async { [weak self] in
229
- FocusableGroupManager.shared.updateFocus(groupId: self?.groupId,
230
- itemId: self?.itemId,
231
- needsForceUpdate: true)
218
+ GroupProxyManager.shared.updateFocus(groupId: self?.groupId,
219
+ itemId: self?.itemId,
220
+ needsForceUpdate: true)
232
221
  }
233
222
  }
234
223
  }
235
224
 
236
225
  override public var canBecomeFocused: Bool {
237
- guard let focusableGroup else {
226
+ guard let focusableGroup = proxy?.getGroup() else {
238
227
  return focusable
239
228
  }
240
229
 
241
230
  return focusableGroup.canBecomeFocusable && focusable
242
231
  }
243
232
  }
244
-
245
- // TODO: Example of solution for future with groupId and focuse id
246
- // func hintLeftFocusId() {
247
- // if let onFocusLeft = dictFromReactNative,
248
- // let groupId = onFocusLeft["groupId"],
249
- // let id = onFocusLeft["id"],
250
- // let view = FocusableGroupManager.item(byGroupId: groupId,
251
- // andItemId: id) {
252
- // _ = addFocusGuide(from: self,
253
- // to: view,
254
- // direction: .left,
255
- // debugMode: true)
256
- // }
257
- // }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-native-apple",
3
- "version": "6.15.2",
3
+ "version": "6.16.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"
@@ -1,181 +0,0 @@
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
-
29
- logFocusError(userInfo: userInfo,
30
- ctx: ctx,
31
- prev: prev,
32
- next: next)
33
- simulateFocusUpdateRequest(prev: prev, next: next)
34
-
35
- // TODO: Imlamentation not need now, uncommit in futer if needed
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
- }
@@ -1,166 +0,0 @@
1
- //
2
- // FocusableGroupManager.swift
3
- // QuickBrickApple
4
- //
5
- // Created by Anton Kononenko on 4/17/19.
6
- // Copyright © 2019 Anton Kononenko. All rights reserved.
7
- //
8
-
9
- import Combine
10
- import Foundation
11
- import React
12
-
13
- protocol FocusableGroupManagerUpdater {
14
- var focusableGroupRegistrationUpdates: PassthroughSubject<FocusableGroupView, Never> { get }
15
- var focusableItemDidUpdatePreferredFocus: PassthroughSubject<FocusableView, Never> { get }
16
- }
17
-
18
- /// Class control focusable group view with focusable items
19
- class FocusableGroupManager {
20
- static let shared = FocusableGroupManager()
21
-
22
- private let groupStorage: FocusableGroupStorage
23
- private let itemStorage: FocusableItemStorage
24
- private let updateService: UpdateService
25
-
26
- private init(groupStorage: FocusableGroupStorage = InMemoryFocusableGroupStorage(),
27
- itemStorage: FocusableItemStorage = InMemoryFocusableItemStorage(),
28
- updateService: UpdateService = UpdateService()) {
29
- self.groupStorage = groupStorage
30
- self.itemStorage = itemStorage
31
- self.updateService = updateService
32
-
33
- // TODO: Unocomment for debugging or catch focus
34
- // registerForMovementDidFailNotification()
35
- }
36
-
37
- // MARK: - Group Managment
38
-
39
- func registerGroup(_ group: FocusableGroupView) async -> Bool {
40
- let wasRegistered = await groupStorage.registerGroup(group)
41
- guard wasRegistered else {
42
- return false
43
- }
44
-
45
- updateService.sendFocusableGroupRegisteredUpdate(group)
46
- return true
47
- }
48
-
49
- func unregisterGroup(withId groupId: String) async {
50
- await groupStorage.unregisterGroup(withId: groupId)
51
- let items = await itemStorage.items(forGroup: groupId)
52
- for itemId in items.keys {
53
- await itemStorage.unregisterItem(withId: itemId, inGroup: groupId)
54
- }
55
- }
56
-
57
- func getGroup(by groupID: String) async -> FocusableGroupView? {
58
- await groupStorage.group(by: groupID)
59
- }
60
-
61
- // MARK: - Item Management
62
-
63
- func registerItem(_ item: FocusableView) async -> Bool {
64
- let success = await itemStorage.registerItem(item)
65
- if success,
66
- let groupId = await item.groupId {
67
- if let itemGroup = await getGroup(by: groupId) {
68
- // Handler in case group was registerd later then items
69
- // TODO: Check this it gives a lot of calls, need to be optimized
70
- updateService.sendFocusableGroupRegisteredUpdate(itemGroup)
71
- }
72
- }
73
- return success
74
- }
75
-
76
- func unregisterItem(withId itemId: String, inGroup groupId: String) async {
77
- await itemStorage.unregisterItem(withId: itemId, inGroup: groupId)
78
- }
79
-
80
- func getItems(forGroup groupId: String) async -> [String: FocusableView] {
81
- await itemStorage.items(forGroup: groupId)
82
- }
83
-
84
- func getItem(withId itemId: String, inGroup groupId: String) async -> FocusableView? {
85
- await itemStorage.item(withId: itemId, inGroup: groupId)
86
- }
87
-
88
- func notifyFocusableItemDidUpdatePreferredFocus(focusableView: FocusableView) {
89
- updateService.sendFocusableItemDidUpdatePreferredFocus(focusableView)
90
- }
91
-
92
- /// Make focus item focusable if exists and registered
93
- ///
94
- /// - Parameters:
95
- /// - groupId: Id of the group
96
- /// - itemId: Id of the focusable item
97
- /// - needsForceUpdate: if value is true after make item as preferred item focus, It will also request Focus Engine to focus immidiately
98
- /// - completion: completion block in case need, that will be called when focusable item did focus
99
- func updateFocus(groupId: String?,
100
- itemId: String?,
101
- needsForceUpdate: Bool = false,
102
- completion: ((Bool) -> Void)? = nil) {
103
- Task { @MainActor in
104
- guard
105
- let groupId,
106
- let itemId,
107
- let groupView = await groupStorage.group(by: groupId),
108
- let viewToFocus = await itemStorage.item(withId: itemId, inGroup: groupId)
109
- else {
110
- completion?(false)
111
- return
112
- }
113
-
114
- groupView.updatePreferredFocusEnv(with: viewToFocus)
115
-
116
- var rootView: UIView? = groupView
117
-
118
- while let unwrapedRootView = rootView,
119
- !unwrapedRootView.isReactRootView() {
120
- rootView = rootView?.superview
121
- }
122
-
123
- guard let rootViewUnwrapped = rootView,
124
- let superView = rootViewUnwrapped.superview as? RCTRootView else {
125
- completion?(false)
126
- return
127
- }
128
-
129
- superView.reactPreferredFocusedView = viewToFocus
130
-
131
- if needsForceUpdate {
132
- superView.setNeedsFocusUpdate()
133
- superView.updateFocusIfNeeded()
134
-
135
- let timeout: TimeInterval = 2.0
136
-
137
- groupView.didFocusCallBack = (completion: {
138
- completion?(true)
139
- groupView.didFocusCallBack = nil
140
-
141
- }, focusableItemId: itemId)
142
-
143
- // Timeout handling
144
- Task {
145
- try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
146
- if groupView.didFocusCallBack != nil {
147
- completion?(false)
148
- groupView.didFocusCallBack = nil
149
- }
150
- }
151
- } else {
152
- completion?(true)
153
- }
154
- }
155
- }
156
- }
157
-
158
- extension FocusableGroupManager: FocusableGroupManagerUpdater {
159
- var focusableItemDidUpdatePreferredFocus: PassthroughSubject<FocusableView, Never> {
160
- updateService.focusableItemDidUpdatePreferredFocus
161
- }
162
-
163
- var focusableGroupRegistrationUpdates: PassthroughSubject<FocusableGroupView, Never> {
164
- updateService.focusableGroupRegistrationUpdates
165
- }
166
- }
@@ -1,37 +0,0 @@
1
- //
2
- // InMemoryFocusableItemStorage.swift
3
- // Pods
4
- //
5
- // Created by Anton Kononenko on 10/28/24.
6
- //
7
-
8
- actor InMemoryFocusableItemStorage: FocusableItemStorage {
9
- private var itemsGroups: [String: [String: FocusableView]] = [:]
10
-
11
- func registerItem(_ item: FocusableView) async -> Bool {
12
- guard let groupId = await item.groupId,
13
- let itemId = await item.itemId else { return false }
14
-
15
- if itemsGroups[groupId] == nil {
16
- itemsGroups[groupId] = [:]
17
- }
18
-
19
- itemsGroups[groupId]?[itemId] = item
20
-
21
- return true
22
- }
23
-
24
- func unregisterItem(withId itemId: String, inGroup groupId: String) {
25
- guard var groupItems = itemsGroups[groupId] else { return }
26
- groupItems[itemId] = nil
27
- itemsGroups[groupId] = groupItems.isEmpty ? nil : groupItems
28
- }
29
-
30
- func items(forGroup groupId: String) -> [String: FocusableView] {
31
- itemsGroups[groupId] ?? [:]
32
- }
33
-
34
- func item(withId itemId: String, inGroup groupId: String) -> FocusableView? {
35
- itemsGroups[groupId]?[itemId]
36
- }
37
- }
@@ -1,24 +0,0 @@
1
- //
2
- // InMemoryGroupStorage.swift
3
- // Pods
4
- //
5
- // Created by Anton Kononenko on 10/28/24.
6
- //
7
-
8
- actor InMemoryFocusableGroupStorage: FocusableGroupStorage {
9
- private var focusableGroups: [String: FocusableGroupView] = [:]
10
-
11
- func registerGroup(_ group: FocusableGroupView) async -> Bool {
12
- guard let id = await group.itemId else { return false }
13
- focusableGroups[id] = group
14
- return true
15
- }
16
-
17
- func unregisterGroup(withId groupId: String) async {
18
- focusableGroups.removeValue(forKey: groupId)
19
- }
20
-
21
- func group(by groupID: String) async -> FocusableGroupView? {
22
- focusableGroups[groupID]
23
- }
24
- }
@@ -1,19 +0,0 @@
1
- //
2
- // FocusableGroupManagerStorages.swift
3
- // Pods
4
- //
5
- // Created by Anton Kononenko on 10/28/24.
6
- //
7
-
8
- protocol FocusableGroupStorage {
9
- func registerGroup(_ group: FocusableGroupView) async -> Bool
10
- func unregisterGroup(withId groupId: String) async
11
- func group(by groupID: String) async -> FocusableGroupView?
12
- }
13
-
14
- protocol FocusableItemStorage {
15
- func registerItem(_ item: FocusableView) async -> Bool
16
- func unregisterItem(withId itemId: String, inGroup groupId: String) async
17
- func items(forGroup groupId: String) async -> [String: FocusableView]
18
- func item(withId itemId: String, inGroup groupId: String) async -> FocusableView?
19
- }
@@ -1,26 +0,0 @@
1
- //
2
- // UpdateService.swift
3
- // Pods
4
- //
5
- // Created by Anton Kononenko on 10/28/24.
6
- //
7
-
8
- import Combine
9
-
10
- struct FocusableGroupUpdateEvent {
11
- let groupId: String
12
- let focusableItems: [String: FocusableView]
13
- }
14
-
15
- class UpdateService {
16
- let focusableGroupRegistrationUpdates = PassthroughSubject<FocusableGroupView, Never>()
17
- let focusableItemDidUpdatePreferredFocus = PassthroughSubject<FocusableView, Never>()
18
-
19
- func sendFocusableGroupRegisteredUpdate(_ focusableGroupView: FocusableGroupView) {
20
- focusableGroupRegistrationUpdates.send(focusableGroupView)
21
- }
22
-
23
- func sendFocusableItemDidUpdatePreferredFocus(_ focusableView: FocusableView) {
24
- focusableItemDidUpdatePreferredFocus.send(focusableView)
25
- }
26
- }