@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.
- package/apple/QuickBrickApple.podspec.json +2 -2
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableManagerModule.swift +8 -8
- package/apple/tvos/Helpers/FocusableGroupManager/GroupProxy.swift +128 -0
- package/apple/tvos/Helpers/FocusableGroupManager/GroupProxyManager.swift +101 -0
- package/apple/tvos/Views/FocusableGroupView/FocusableGroupView.swift +27 -39
- package/apple/tvos/Views/FocusableView/FocusableView.swift +44 -69
- package/package.json +1 -1
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableGroupManager+MovementDidFailNotification.swift +0 -181
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableGroupManager.swift +0 -166
- package/apple/tvos/Helpers/FocusableGroupManager/InMemoryFocusableItemStorage.swift +0 -37
- package/apple/tvos/Helpers/FocusableGroupManager/InMemoryGroupStorage.swift +0 -24
- package/apple/tvos/Helpers/FocusableGroupManager/Protocols/FocusableGroupManagerStorages.swift +0 -19
- package/apple/tvos/Helpers/FocusableGroupManager/Protocols/UpdateService.swift +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "QuickBrickApple",
|
|
3
|
-
"version": "6.
|
|
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.
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
140
|
+
cleanup()
|
|
141
|
+
}
|
|
154
142
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
deinit {
|
|
144
|
+
cleanup()
|
|
145
|
+
}
|
|
158
146
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
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
|
-
}
|
package/apple/tvos/Helpers/FocusableGroupManager/Protocols/FocusableGroupManagerStorages.swift
DELETED
|
@@ -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
|
-
}
|