@applicaster/quick-brick-native-apple 6.9.4 → 6.9.5
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.swift +117 -116
- package/apple/tvos/Helpers/FocusableGroupManager/FocusableManagerModule.swift +8 -7
- package/apple/tvos/Helpers/FocusableGroupManager/InMemoryFocusableItemStorage.swift +37 -0
- package/apple/tvos/Helpers/FocusableGroupManager/InMemoryGroupStorage.swift +24 -0
- package/apple/tvos/Helpers/FocusableGroupManager/Protocols/FocusableGroupManagerStorages.swift +19 -0
- package/apple/tvos/Helpers/FocusableGroupManager/Protocols/UpdateService.swift +29 -0
- package/apple/tvos/ReactNativeModulesExportstvOS.m +0 -1
- package/apple/tvos/Views/FocusableGroupView/FocusableGroupView.swift +119 -100
- package/apple/tvos/Views/FocusableGroupView/FocusableGroupViewModule.swift +1 -1
- package/apple/tvos/Views/FocusableView/FocusableView.swift +91 -62
- package/apple/tvos/Views/FocusableView/FocusableViewModule.swift +1 -1
- package/apple/tvos/Views/ParallaxView/Protocols/FocusableGroupProtocol.swift +11 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "QuickBrickApple",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.5",
|
|
4
4
|
"platforms": {
|
|
5
5
|
"ios": "14.0",
|
|
6
6
|
"tvos": "14.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.9.
|
|
19
|
+
"tag": "@@applicaster/quick-brick-native-apple/6.9.5"
|
|
20
20
|
},
|
|
21
21
|
"requires_arc": true,
|
|
22
22
|
"source_files": "universal/**/*.{m,swift}",
|
|
@@ -6,109 +6,92 @@
|
|
|
6
6
|
// Copyright © 2019 Anton Kononenko. All rights reserved.
|
|
7
7
|
//
|
|
8
8
|
|
|
9
|
+
import Combine
|
|
9
10
|
import Foundation
|
|
10
11
|
import React
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var itemsGroups: [String: [String: FocusableView]] = [:]
|
|
13
|
+
protocol FocusableGroupManagerUpdater {
|
|
14
|
+
var focusableGroupRegistrationUpdates: PassthroughSubject<FocusableGroupView, Never> { get }
|
|
15
|
+
var focusableItemsUpdatedAddedToGroupUpdate: PassthroughSubject<FocusableGroupUpdateEvent, Never> { get }
|
|
16
|
+
}
|
|
17
17
|
|
|
18
18
|
/// Class control focusable group view with focusable items
|
|
19
19
|
class FocusableGroupManager {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
|
|
34
|
+
// MARK: - Group Managment
|
|
35
|
+
|
|
36
|
+
public func registerGroup(_ group: FocusableGroupView) async -> Bool {
|
|
37
|
+
let wasRegistered = await groupStorage.registerGroup(group)
|
|
38
|
+
guard wasRegistered else {
|
|
27
39
|
return false
|
|
28
40
|
}
|
|
29
|
-
var newItemsGroup: [String: FocusableView] = [:]
|
|
30
|
-
if let itemsGroup = itemsGroups[groupId] {
|
|
31
|
-
newItemsGroup = itemsGroup
|
|
32
|
-
}
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
itemsGroups[groupId] = newItemsGroup
|
|
36
|
-
notifyGroupView(groupID: groupId)
|
|
42
|
+
updateService.sendFocusableGroupRegisteredUpdate(group)
|
|
37
43
|
return true
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
public func unregisterGroup(withId groupId: String) async {
|
|
47
|
+
await groupStorage.unregisterGroup(withId: groupId)
|
|
48
|
+
let items = await itemStorage.items(forGroup: groupId)
|
|
49
|
+
for itemId in items.keys {
|
|
50
|
+
await itemStorage.unregisterItem(withId: itemId, inGroup: groupId)
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
itemsGroups[groupId] = newItemsGroup
|
|
53
|
+
await notifyGroupView(groupId: groupId)
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
focusableGroups[itemId] = nil
|
|
56
|
+
public func getGroup(by groupID: String) async -> FocusableGroupView? {
|
|
57
|
+
await groupStorage.group(by: groupID)
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
///
|
|
57
|
-
/// - Parameter item: FocusableGroup instance
|
|
58
|
-
class func registerFocusableGroup(group: FocusableGroupView) -> Bool {
|
|
59
|
-
var retVal = false
|
|
60
|
+
// MARK: - Item Management
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
public func registerItem(_ item: FocusableView) async -> Bool {
|
|
63
|
+
let success = await itemStorage.registerItem(item)
|
|
64
|
+
if success,
|
|
65
|
+
let groupId = await item.groupId {
|
|
66
|
+
await notifyGroupView(groupId: groupId)
|
|
67
|
+
if let itemGroup = await getGroup(by: groupId) {
|
|
68
|
+
// Handler in case group was registerd later then items
|
|
69
|
+
updateService.sendFocusableGroupRegisteredUpdate(itemGroup)
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
|
-
return
|
|
72
|
+
return success
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/// - Returns: FocusableGroupView instance if registered into manager
|
|
72
|
-
class func group(by groupID: String) -> FocusableGroupView? {
|
|
73
|
-
guard let groupView = focusableGroups[groupID] else {
|
|
74
|
-
return nil
|
|
75
|
-
}
|
|
76
|
-
return groupView
|
|
75
|
+
public func unregisterItem(withId itemId: String, inGroup groupId: String) async {
|
|
76
|
+
await itemStorage.unregisterItem(withId: itemId, inGroup: groupId)
|
|
77
|
+
await notifyGroupView(groupId: groupId)
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/// - Parameter groupID: ID of the group
|
|
82
|
-
/// - Returns: Dictionary in format [ViewID:FocusableViewIntance]
|
|
83
|
-
class func itemsForGroup(by groupID: String) -> [String: FocusableView] {
|
|
84
|
-
guard let groupViewsDict = itemsGroups[groupID] else {
|
|
85
|
-
return [:]
|
|
86
|
-
}
|
|
87
|
-
return groupViewsDict
|
|
80
|
+
public func getItems(forGroup groupId: String) async -> [String: FocusableView] {
|
|
81
|
+
await itemStorage.items(forGroup: groupId)
|
|
88
82
|
}
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/// - Parameter groupID: ID of the group
|
|
93
|
-
class func notifyGroupView(groupID: String) {
|
|
94
|
-
DispatchQueue.main.async {
|
|
95
|
-
guard let groupView = focusableGroups[groupID] else {
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
let groupItems = itemsForGroup(by: groupID)
|
|
99
|
-
groupView.groupItemsUpdated(groupItems: groupItems)
|
|
100
|
-
}
|
|
84
|
+
public func getItem(withId itemId: String, inGroup groupId: String) async -> FocusableView? {
|
|
85
|
+
await itemStorage.item(withId: itemId, inGroup: groupId)
|
|
101
86
|
}
|
|
102
87
|
|
|
103
|
-
///
|
|
88
|
+
/// Notify group view that item relevant to it was updated
|
|
104
89
|
///
|
|
105
|
-
/// -
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
andItemId itemId: String) -> FocusableView? {
|
|
111
|
-
itemsForGroup(by: groupId).first(where: { $0.key == itemId })?.value
|
|
90
|
+
/// - Parameter groupID: ID of the group
|
|
91
|
+
private func notifyGroupView(groupId: String) async {
|
|
92
|
+
let groupItems = await getItems(forGroup: groupId)
|
|
93
|
+
updateService.sendFocusableItemInGroupUpdate(groupId: groupId,
|
|
94
|
+
focusableItems: groupItems)
|
|
112
95
|
}
|
|
113
96
|
|
|
114
97
|
/// Make focus item focusable if exists and registered
|
|
@@ -118,53 +101,71 @@ class FocusableGroupManager {
|
|
|
118
101
|
/// - itemId: Id of the focusable item
|
|
119
102
|
/// - needsForceUpdate: if value is true after make item as preferred item focus, It will also request Focus Engine to focus immidiately
|
|
120
103
|
/// - completion: completion block in case need, that will be called when focusable item did focus
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
} else {
|
|
136
|
-
rootView = nil
|
|
137
|
-
}
|
|
138
|
-
}
|
|
104
|
+
public func updateFocus(groupId: String?,
|
|
105
|
+
itemId: String?,
|
|
106
|
+
needsForceUpdate: Bool = false,
|
|
107
|
+
completion: ((Bool) -> Void)? = nil) {
|
|
108
|
+
Task { @MainActor in
|
|
109
|
+
guard
|
|
110
|
+
let groupId,
|
|
111
|
+
let itemId,
|
|
112
|
+
let groupView = await groupStorage.group(by: groupId),
|
|
113
|
+
let viewToFocus = await itemStorage.item(withId: itemId, inGroup: groupId)
|
|
114
|
+
else {
|
|
115
|
+
completion?(false)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
139
118
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
groupView.updatePreferredFocusEnv(with: viewToFocus)
|
|
120
|
+
|
|
121
|
+
var rootView: UIView? = groupView
|
|
122
|
+
|
|
123
|
+
while rootView != nil,
|
|
124
|
+
!rootView?.isReactRootView() {
|
|
125
|
+
rootView = rootView?.superview
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
guard let rootViewUnwrapped = rootView,
|
|
129
|
+
let superView = rootViewUnwrapped.superview as? RCTRootView else {
|
|
130
|
+
completion?(false)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
superView.reactPreferredFocusedView = viewToFocus
|
|
135
|
+
|
|
136
|
+
if needsForceUpdate {
|
|
137
|
+
superView.setNeedsFocusUpdate()
|
|
138
|
+
superView.updateFocusIfNeeded()
|
|
139
|
+
|
|
140
|
+
let timeout: TimeInterval = 2.0
|
|
141
|
+
|
|
142
|
+
groupView.didFocusCallBack = (completion: {
|
|
143
|
+
completion?(true)
|
|
144
|
+
groupView.didFocusCallBack = nil
|
|
145
|
+
|
|
146
|
+
}, focusableItemId: itemId)
|
|
143
147
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
var completionWasCalled = false
|
|
151
|
-
|
|
152
|
-
// Timeout 2 seconds if did focus was not called by some reason, we are passing completion
|
|
153
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
154
|
-
if completionWasCalled == false {
|
|
155
|
-
completion?(false)
|
|
156
|
-
groupView.didFocusCallBack = nil
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
groupView.didFocusCallBack = (completion: {
|
|
161
|
-
completionWasCalled = true
|
|
162
|
-
completion?(true)
|
|
163
|
-
|
|
164
|
-
}, focusableItemId: itemId)
|
|
148
|
+
// Timeout handling
|
|
149
|
+
Task {
|
|
150
|
+
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
|
151
|
+
if groupView.didFocusCallBack != nil {
|
|
152
|
+
completion?(false)
|
|
153
|
+
groupView.didFocusCallBack = nil
|
|
165
154
|
}
|
|
166
155
|
}
|
|
156
|
+
} else {
|
|
157
|
+
completion?(true)
|
|
167
158
|
}
|
|
168
159
|
}
|
|
169
160
|
}
|
|
170
161
|
}
|
|
162
|
+
|
|
163
|
+
extension FocusableGroupManager: FocusableGroupManagerUpdater {
|
|
164
|
+
var focusableGroupRegistrationUpdates: PassthroughSubject<FocusableGroupView, Never> {
|
|
165
|
+
updateService.focusableGroupRegistrationUpdates
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
var focusableItemsUpdatedAddedToGroupUpdate: PassthroughSubject<FocusableGroupUpdateEvent, Never> {
|
|
169
|
+
updateService.focusableItemsUpdatedAddedToGroupUpdate
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -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.updateFocus(groupId,
|
|
48
|
-
|
|
47
|
+
FocusableGroupManager.shared.updateFocus(groupId: groupId,
|
|
48
|
+
itemId: itemId)
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -60,12 +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.updateFocus(groupId,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
FocusableGroupManager.shared.updateFocus(groupId: groupId,
|
|
64
|
+
itemId: itemId,
|
|
65
|
+
needsForceUpdate: true,
|
|
66
|
+
completion: { succeed in
|
|
67
|
+
callback?([succeed])
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
})
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 focusableItemsUpdatedAddedToGroupUpdate = PassthroughSubject<FocusableGroupUpdateEvent, Never>()
|
|
18
|
+
|
|
19
|
+
public func sendFocusableGroupRegisteredUpdate(_ focusableGroupView: FocusableGroupView) {
|
|
20
|
+
focusableGroupRegistrationUpdates.send(focusableGroupView)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public func sendFocusableItemInGroupUpdate(groupId: String,
|
|
24
|
+
focusableItems: [String: FocusableView]) {
|
|
25
|
+
let event = FocusableGroupUpdateEvent(groupId: groupId,
|
|
26
|
+
focusableItems: focusableItems)
|
|
27
|
+
focusableItemsUpdatedAddedToGroupUpdate.send(event)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -26,7 +26,6 @@ RCT_EXPORT_VIEW_PROPERTY(initialItemId, NSString);
|
|
|
26
26
|
RCT_EXPORT_VIEW_PROPERTY(resetFocusToInitialValue, BOOL);
|
|
27
27
|
RCT_EXPORT_VIEW_PROPERTY(isFocusDisabled, BOOL);
|
|
28
28
|
RCT_EXPORT_VIEW_PROPERTY(dependantGroupIds, NSArray)
|
|
29
|
-
RCT_EXPORT_VIEW_PROPERTY(isManuallyBlockingFocusValue, NSNumber)
|
|
30
29
|
@end
|
|
31
30
|
|
|
32
31
|
@interface RCT_EXTERN_MODULE(FocusableViewModule, RCTViewManager)
|
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
// Copyright © 2019 Kononenko. All rights reserved.
|
|
7
7
|
//
|
|
8
8
|
|
|
9
|
+
import Combine
|
|
9
10
|
import Foundation
|
|
10
11
|
import React
|
|
11
12
|
import UIKit
|
|
12
13
|
|
|
13
14
|
/// Focusable Group View that implements UIFocusGuide instance that catches focus event
|
|
14
15
|
public class FocusableGroupView: RCTTVView {
|
|
15
|
-
|
|
16
|
-
var didFocusCallBack: (completion: () -> Void, focusableItemId: String)?
|
|
17
|
-
|
|
18
|
-
/// This parameter blocks focus behavior without user interaction, it is used to prevent fast jumps between items
|
|
19
|
-
@objc public var isManuallyBlockingFocusValue: NSNumber?
|
|
16
|
+
private var cancellables = Set<AnyCancellable>()
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
/// Completion that will be used when focus manager forcing to update focusable group
|
|
19
|
+
public var didFocusCallBack: (completion: () -> Void, focusableItemId: String)?
|
|
22
20
|
|
|
23
21
|
/// Notify React-Native environment that Focus Did Update
|
|
24
22
|
@objc public var onDidUpdateFocus: RCTBubblingEventBlock?
|
|
@@ -42,7 +40,9 @@ public class FocusableGroupView: RCTTVView {
|
|
|
42
40
|
@objc public var itemId: String? {
|
|
43
41
|
didSet {
|
|
44
42
|
if itemId != oldValue {
|
|
45
|
-
|
|
43
|
+
Task(priority: .userInitiated) {
|
|
44
|
+
await manager.registerGroup(self)
|
|
45
|
+
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -50,8 +50,12 @@ public class FocusableGroupView: RCTTVView {
|
|
|
50
50
|
/// ID of the parent group, if relevant
|
|
51
51
|
@objc public var groupId: String?
|
|
52
52
|
|
|
53
|
+
private var manager: FocusableGroupManager {
|
|
54
|
+
FocusableGroupManager.shared
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
/// If this variable not nil it overrides default preffered focus environment
|
|
54
|
-
var customPrefferedFocusEnvironment: [UIFocusEnvironment]? {
|
|
58
|
+
@MainActor private var customPrefferedFocusEnvironment: [UIFocusEnvironment]? {
|
|
55
59
|
didSet {
|
|
56
60
|
var rootView: UIView? = self
|
|
57
61
|
|
|
@@ -76,7 +80,17 @@ public class FocusableGroupView: RCTTVView {
|
|
|
76
80
|
|
|
77
81
|
/// Check if group has an initial focus
|
|
78
82
|
/// Note: In case Initial init when app start not calling shouldFocusUpdate
|
|
79
|
-
var isGroupWasFocusedByUser = false
|
|
83
|
+
private var isGroupWasFocusedByUser = false
|
|
84
|
+
|
|
85
|
+
/// Manager that connects View instance to FocusableGroupViewModule
|
|
86
|
+
private weak var module: FocusableGroupViewModule?
|
|
87
|
+
public func setModule(_ module: FocusableGroupViewModule) {
|
|
88
|
+
self.module = module
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override init(bridge: RCTBridge) {
|
|
92
|
+
super.init(bridge: bridge)
|
|
93
|
+
}
|
|
80
94
|
|
|
81
95
|
override public func removeFromSuperview() {
|
|
82
96
|
super.removeFromSuperview()
|
|
@@ -84,14 +98,58 @@ public class FocusableGroupView: RCTTVView {
|
|
|
84
98
|
return
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
Task {
|
|
102
|
+
// Removing when react native releases the view
|
|
103
|
+
await manager.unregisterGroup(withId: itemId)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public required init?(coder aDecoder: NSCoder) {
|
|
108
|
+
super.init(coder: aDecoder)
|
|
109
|
+
setupFocus()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override public init(frame: CGRect) {
|
|
113
|
+
super.init(frame: frame)
|
|
114
|
+
setupFocus()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func initializeFocusGuideIfNeeded() {
|
|
118
|
+
if focusGuide == nil {
|
|
119
|
+
focusGuide = UIFocusGuide()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Update customPrefferedFocusEnvironment
|
|
124
|
+
///
|
|
125
|
+
/// - Parameter view: view instance that should be preffered
|
|
126
|
+
@MainActor func updatePreferredFocusEnv(with view: UIFocusEnvironment) {
|
|
127
|
+
customPrefferedFocusEnvironment = [view]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Setup Focus Guide for use
|
|
131
|
+
private func setupFocus() {
|
|
132
|
+
initializeFocusGuideIfNeeded()
|
|
133
|
+
|
|
134
|
+
addLayoutGuide(focusGuide)
|
|
135
|
+
|
|
136
|
+
focusGuide.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
|
|
137
|
+
focusGuide.topAnchor.constraint(equalTo: topAnchor).isActive = true
|
|
138
|
+
focusGuide.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
|
|
139
|
+
focusGuide.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
|
|
140
|
+
|
|
141
|
+
manager.focusableItemsUpdatedAddedToGroupUpdate
|
|
142
|
+
.filter { $0.groupId == self.groupId }
|
|
143
|
+
.sink { [weak self] event in
|
|
144
|
+
guard let self else { return }
|
|
145
|
+
groupItemsUpdated(groupItems: event.focusableItems)
|
|
146
|
+
}.store(in: &cancellables)
|
|
89
147
|
}
|
|
90
148
|
|
|
91
149
|
/// View connected to GroupView was updated
|
|
92
150
|
///
|
|
93
151
|
/// - Parameter groupItems: dictionary connected to group view
|
|
94
|
-
|
|
152
|
+
@MainActor private func groupItemsUpdated(groupItems: [String: FocusableView]) {
|
|
95
153
|
guard
|
|
96
154
|
isGroupWasFocusedByUser == false,
|
|
97
155
|
let initialItemId,
|
|
@@ -102,7 +160,7 @@ public class FocusableGroupView: RCTTVView {
|
|
|
102
160
|
|
|
103
161
|
let focusedView = customPrefferedFocusEnvironment?.first as? UIView
|
|
104
162
|
if focusedView != initialView || focusedView == nil {
|
|
105
|
-
|
|
163
|
+
updatePreferredFocusEnv(with: initialView)
|
|
106
164
|
}
|
|
107
165
|
}
|
|
108
166
|
|
|
@@ -110,7 +168,7 @@ public class FocusableGroupView: RCTTVView {
|
|
|
110
168
|
///
|
|
111
169
|
/// - Parameter nextFocusedItem: next item that should be focused
|
|
112
170
|
/// - Returns: true if reser succeed
|
|
113
|
-
func resetFocusPrefferedEnvironmentIfNeeded(nextFocusedItem: UIFocusItem?) -> Bool {
|
|
171
|
+
private func resetFocusPrefferedEnvironmentIfNeeded(nextFocusedItem: UIFocusItem?) async -> Bool {
|
|
114
172
|
var retVal = false
|
|
115
173
|
guard let nextFocusedItem else {
|
|
116
174
|
return retVal
|
|
@@ -119,11 +177,11 @@ public class FocusableGroupView: RCTTVView {
|
|
|
119
177
|
if focusItemIsDescendant(nextFocuseItem: nextFocusedItem) == false {
|
|
120
178
|
if resetFocusToInitialValue,
|
|
121
179
|
let initialItemId {
|
|
122
|
-
if tryTakePrefferedViewFromDependantGroups() == false {
|
|
180
|
+
if await tryTakePrefferedViewFromDependantGroups() == false {
|
|
123
181
|
if let groupId,
|
|
124
|
-
let initialItemView =
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
let initialItemView = await manager.getItem(withId: groupId,
|
|
183
|
+
inGroup: initialItemId) {
|
|
184
|
+
updatePreferredFocusEnv(with: initialItemView)
|
|
127
185
|
retVal = true
|
|
128
186
|
}
|
|
129
187
|
} else {
|
|
@@ -137,68 +195,26 @@ public class FocusableGroupView: RCTTVView {
|
|
|
137
195
|
/// Try to focus on preffered item from another group
|
|
138
196
|
///
|
|
139
197
|
/// - Returns: true if can focus otherwise false
|
|
140
|
-
func tryTakePrefferedViewFromDependantGroups() -> Bool {
|
|
198
|
+
private func tryTakePrefferedViewFromDependantGroups() async -> Bool {
|
|
141
199
|
guard let initialItemId,
|
|
142
200
|
let dependantGroupIds,
|
|
143
201
|
|
|
144
202
|
let firstGroupId = dependantGroupIds.first,
|
|
145
|
-
let initialView =
|
|
146
|
-
|
|
203
|
+
let initialView = await manager.getItem(withId: firstGroupId,
|
|
204
|
+
inGroup: initialItemId)
|
|
147
205
|
else {
|
|
148
206
|
return false
|
|
149
207
|
}
|
|
150
208
|
|
|
151
|
-
|
|
209
|
+
updatePreferredFocusEnv(with: initialView)
|
|
152
210
|
|
|
153
211
|
return true
|
|
154
212
|
}
|
|
155
213
|
|
|
156
|
-
/// Manager that connects View instance to FocusableGroupViewModule
|
|
157
|
-
var manager: FocusableGroupViewModule?
|
|
158
|
-
|
|
159
|
-
override init(bridge: RCTBridge) {
|
|
160
|
-
super.init(bridge: bridge)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
public required init?(coder aDecoder: NSCoder) {
|
|
164
|
-
super.init(coder: aDecoder)
|
|
165
|
-
setupFocus()
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
override public init(frame: CGRect) {
|
|
169
|
-
super.init(frame: frame)
|
|
170
|
-
setupFocus()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
func initializeFocusGuideIfNeeded() {
|
|
174
|
-
if focusGuide == nil {
|
|
175
|
-
focusGuide = UIFocusGuide()
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// Update customPrefferedFocusEnvironment
|
|
180
|
-
///
|
|
181
|
-
/// - Parameter view: view instance that should be preffered
|
|
182
|
-
func updatePrefferedFocusEnv(with view: FocusableView) {
|
|
183
|
-
customPrefferedFocusEnvironment = [view]
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/// Setup Focus Guide for use
|
|
187
|
-
func setupFocus() {
|
|
188
|
-
initializeFocusGuideIfNeeded()
|
|
189
|
-
|
|
190
|
-
addLayoutGuide(focusGuide)
|
|
191
|
-
|
|
192
|
-
focusGuide.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
|
|
193
|
-
focusGuide.topAnchor.constraint(equalTo: topAnchor).isActive = true
|
|
194
|
-
focusGuide.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
|
|
195
|
-
focusGuide.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
|
|
196
|
-
}
|
|
197
|
-
|
|
198
214
|
/// Send update focus event to Reactnative
|
|
199
215
|
///
|
|
200
216
|
/// - Parameter context: Update UIFocusUpdateContext instance
|
|
201
|
-
func sendUpdateFocusEventToReactNative(bubbleEventBlock: RCTBubblingEventBlock?, context: UIFocusUpdateContext) {
|
|
217
|
+
private func sendUpdateFocusEventToReactNative(bubbleEventBlock: RCTBubblingEventBlock?, context: UIFocusUpdateContext) async {
|
|
202
218
|
guard let bubbleEventBlock else {
|
|
203
219
|
return
|
|
204
220
|
}
|
|
@@ -225,9 +241,9 @@ public class FocusableGroupView: RCTTVView {
|
|
|
225
241
|
if resetFocusToInitialValue,
|
|
226
242
|
let initialItemId {
|
|
227
243
|
if let groupId {
|
|
228
|
-
let groupItems =
|
|
244
|
+
let groupItems = await manager.getItems(forGroup: groupId)
|
|
229
245
|
if let initialItemView = groupItems[initialItemId] {
|
|
230
|
-
|
|
246
|
+
updatePreferredFocusEnv(with: initialItemView)
|
|
231
247
|
}
|
|
232
248
|
}
|
|
233
249
|
}
|
|
@@ -241,7 +257,7 @@ public class FocusableGroupView: RCTTVView {
|
|
|
241
257
|
///
|
|
242
258
|
/// - Parameter focusItem: focu item instance
|
|
243
259
|
/// - Returns: dictionary instance in case data exists, otherwise nil
|
|
244
|
-
func dataForFocusItem(focusItem: Any?) -> [String: Any]? {
|
|
260
|
+
private func dataForFocusItem(focusItem: Any?) -> [String: Any]? {
|
|
245
261
|
var retVal = [String: Any]()
|
|
246
262
|
if let focusedView = focusItem as? RCTTVView {
|
|
247
263
|
retVal[FocusItemDataKeys.describingView] = String(describing: focusedView)
|
|
@@ -256,21 +272,22 @@ public class FocusableGroupView: RCTTVView {
|
|
|
256
272
|
/// Update preffered Focus View for focuse guide if next focuasable view is part of this focus guide
|
|
257
273
|
///
|
|
258
274
|
/// - Parameter nextFocuseItem: next focus item instance
|
|
259
|
-
func updatePreferredFocusView(nextFocuseItem:
|
|
260
|
-
guard let nextFocusView = nextFocuseItem
|
|
261
|
-
nextFocusView
|
|
275
|
+
@MainActor func updatePreferredFocusView(nextFocuseItem: UIFocusEnvironment?) {
|
|
276
|
+
guard let nextFocusView = nextFocuseItem,
|
|
277
|
+
let asView = nextFocusView as? UIView,
|
|
278
|
+
asView.isDescendant(of: self)
|
|
262
279
|
else {
|
|
263
280
|
return
|
|
264
281
|
}
|
|
265
282
|
|
|
266
|
-
|
|
283
|
+
updatePreferredFocusEnv(with: nextFocusView)
|
|
267
284
|
}
|
|
268
285
|
|
|
269
286
|
/// Checks if next focus item is part of the current group
|
|
270
287
|
///
|
|
271
288
|
/// - Parameter nextFocuseItem: next item to focus
|
|
272
289
|
/// - Returns: true if next focus item part of this group instance
|
|
273
|
-
func focusItemIsDescendant(nextFocuseItem: Any?) -> Bool {
|
|
290
|
+
private func focusItemIsDescendant(nextFocuseItem: Any?) -> Bool {
|
|
274
291
|
guard let nextFocusView = nextFocuseItem as? UIView,
|
|
275
292
|
nextFocusView.isDescendant(of: self)
|
|
276
293
|
else {
|
|
@@ -283,7 +300,7 @@ public class FocusableGroupView: RCTTVView {
|
|
|
283
300
|
///
|
|
284
301
|
/// - Parameter focusHeading: UIFocusHeading enum
|
|
285
302
|
/// - Returns: Readable string from enum
|
|
286
|
-
func focusHeadingToString(focusHeading: UIFocusHeading) -> String {
|
|
303
|
+
private func focusHeadingToString(focusHeading: UIFocusHeading) -> String {
|
|
287
304
|
switch focusHeading {
|
|
288
305
|
case UIFocusHeading.up:
|
|
289
306
|
FocusHeadingTextValues.up
|
|
@@ -302,18 +319,6 @@ public class FocusableGroupView: RCTTVView {
|
|
|
302
319
|
}
|
|
303
320
|
}
|
|
304
321
|
|
|
305
|
-
/// Force manually focus update
|
|
306
|
-
func manuallyBlockFocus() {
|
|
307
|
-
if let isManuallyBlockingFocusValue {
|
|
308
|
-
isManuallyBlockingFocus = true
|
|
309
|
-
let delay = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(isManuallyBlockingFocusValue.floatValue * 1000))
|
|
310
|
-
|
|
311
|
-
DispatchQueue.main.asyncAfter(deadline: delay) { [weak self] in
|
|
312
|
-
self?.isManuallyBlockingFocus = false
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
322
|
// MARK: Focus Engine
|
|
318
323
|
|
|
319
324
|
override public var preferredFocusEnvironments: [UIFocusEnvironment] {
|
|
@@ -324,25 +329,30 @@ public class FocusableGroupView: RCTTVView {
|
|
|
324
329
|
}
|
|
325
330
|
|
|
326
331
|
override public func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
|
|
327
|
-
guard isFocusDisabled == false
|
|
328
|
-
isManuallyBlockingFocus == false
|
|
332
|
+
guard isFocusDisabled == false
|
|
329
333
|
else {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
sendUpdateFocusEventToReactNative(bubbleEventBlock: onWillUpdateFocus,
|
|
333
|
-
|
|
334
|
+
Task.detached(priority: .userInitiated) { [weak self] in
|
|
335
|
+
guard let self else { return }
|
|
336
|
+
await sendUpdateFocusEventToReactNative(bubbleEventBlock: onWillUpdateFocus,
|
|
337
|
+
context: context)
|
|
334
338
|
}
|
|
339
|
+
|
|
335
340
|
return false
|
|
336
341
|
}
|
|
337
342
|
|
|
338
343
|
isGroupWasFocusedByUser = true
|
|
339
344
|
|
|
340
|
-
|
|
341
|
-
|
|
345
|
+
Task(priority: .userInitiated) {
|
|
346
|
+
if await resetFocusPrefferedEnvironmentIfNeeded(nextFocusedItem: context.nextFocusedItem) == false {
|
|
347
|
+
updatePreferredFocusView(nextFocuseItem: context.nextFocusedItem)
|
|
348
|
+
}
|
|
342
349
|
}
|
|
343
350
|
|
|
344
|
-
|
|
345
|
-
|
|
351
|
+
Task.detached(priority: .userInitiated) { [weak self] in
|
|
352
|
+
guard let self else { return }
|
|
353
|
+
await sendUpdateFocusEventToReactNative(bubbleEventBlock: onWillUpdateFocus,
|
|
354
|
+
context: context)
|
|
355
|
+
}
|
|
346
356
|
|
|
347
357
|
if focusItemIsDescendant(nextFocuseItem: context.nextFocusedItem) == false {
|
|
348
358
|
isFocusDisabled = false
|
|
@@ -352,16 +362,19 @@ public class FocusableGroupView: RCTTVView {
|
|
|
352
362
|
|
|
353
363
|
override public func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
|
|
354
364
|
super.didUpdateFocus(in: context, with: coordinator)
|
|
355
|
-
manuallyBlockFocus()
|
|
356
365
|
tryDidFocusCallCallback(context: context)
|
|
357
|
-
|
|
358
|
-
|
|
366
|
+
|
|
367
|
+
Task.detached(priority: .userInitiated) { [weak self] in
|
|
368
|
+
guard let self else { return }
|
|
369
|
+
await sendUpdateFocusEventToReactNative(bubbleEventBlock: onDidUpdateFocus,
|
|
370
|
+
context: context)
|
|
371
|
+
}
|
|
359
372
|
}
|
|
360
373
|
|
|
361
374
|
/// Try to send a callback in case focus manager request callback during force focus update
|
|
362
375
|
///
|
|
363
376
|
/// - Parameter context: An instance of UIFocusUpdateContext containing metadata of the focus related update.
|
|
364
|
-
func tryDidFocusCallCallback(context: UIFocusUpdateContext) {
|
|
377
|
+
private func tryDidFocusCallCallback(context: UIFocusUpdateContext) {
|
|
365
378
|
guard let callbackData = didFocusCallBack,
|
|
366
379
|
let fousedView = context.nextFocusedView as? FocusableView,
|
|
367
380
|
let itemId = fousedView.itemId
|
|
@@ -379,3 +392,9 @@ public class FocusableGroupView: RCTTVView {
|
|
|
379
392
|
didFocusCallBack = nil
|
|
380
393
|
}
|
|
381
394
|
}
|
|
395
|
+
|
|
396
|
+
extension FocusableGroupView: FocusableGroupProtocol {
|
|
397
|
+
var canBacomeFocusable: Bool {
|
|
398
|
+
!isFocusDisabled
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -6,13 +6,21 @@
|
|
|
6
6
|
// Copyright © 2019 Anton Kononenko. All rights reserved.
|
|
7
7
|
//
|
|
8
8
|
|
|
9
|
+
import Combine
|
|
9
10
|
import Foundation
|
|
10
11
|
import React
|
|
11
12
|
import UIKit
|
|
12
13
|
|
|
13
14
|
/// RCTTVView subclass that has api how to conects to FocusableGroup
|
|
14
15
|
public class FocusableView: ParallaxView {
|
|
15
|
-
|
|
16
|
+
private var cancellables = Set<AnyCancellable>()
|
|
17
|
+
|
|
18
|
+
private weak var module: FocusableViewModule?
|
|
19
|
+
func setModule(_ module: FocusableViewModule) {
|
|
20
|
+
self.module = module
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@MainActor private weak var focusableGroup: FocusableGroupProtocol?
|
|
16
24
|
|
|
17
25
|
@objc public var onViewFocus: RCTBubblingEventBlock?
|
|
18
26
|
@objc public var onViewPress: RCTBubblingEventBlock?
|
|
@@ -32,22 +40,67 @@ public class FocusableView: ParallaxView {
|
|
|
32
40
|
/// Define if view can become focused
|
|
33
41
|
@objc open var focusable = true
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
private var manager: FocusableGroupManager {
|
|
44
|
+
FocusableGroupManager.shared
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@MainActor @objc public var preferredFocus: Bool = false {
|
|
36
48
|
didSet {
|
|
37
49
|
guard preferredFocus else {
|
|
38
50
|
return
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
focusableGroup.updatePrefferedFocusEnv(with: self!)
|
|
45
|
-
}
|
|
52
|
+
|
|
53
|
+
if let focusableGroup {
|
|
54
|
+
// Update Prefered focus view in group
|
|
55
|
+
focusableGroup.updatePreferredFocusEnv(with: self)
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
var isFocusLayoutConfigured = false
|
|
60
|
+
private var isFocusLayoutConfigured = false
|
|
61
|
+
|
|
62
|
+
/// Define if view was registered
|
|
63
|
+
private var isViewRegistered: Bool = false
|
|
64
|
+
|
|
65
|
+
override public init(frame: CGRect) {
|
|
66
|
+
super.init(frame: frame)
|
|
67
|
+
initialize()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public required init?(coder aDecoder: NSCoder) {
|
|
71
|
+
super.init(coder: aDecoder)
|
|
72
|
+
initialize()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Initialize component
|
|
76
|
+
private func initialize() {
|
|
77
|
+
delegate = self
|
|
78
|
+
|
|
79
|
+
manager.focusableGroupRegistrationUpdates
|
|
80
|
+
.filter { $0.itemId == self.groupId }
|
|
81
|
+
.sink { [weak self] focusableGroup in
|
|
82
|
+
guard let self else { return }
|
|
83
|
+
DispatchQueue.main.async {
|
|
84
|
+
self.focusableGroup = focusableGroup
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
}.store(in: &cancellables)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// ID of the View provided by React-Native env
|
|
91
|
+
@objc private(set) var itemId: String? {
|
|
92
|
+
didSet {
|
|
93
|
+
registerView()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// ID of the View provided by React-Native env
|
|
98
|
+
@objc private(set) var groupId: String? {
|
|
99
|
+
didSet {
|
|
100
|
+
registerView()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
51
104
|
override public func layoutSubviews() {
|
|
52
105
|
super.layoutSubviews()
|
|
53
106
|
|
|
@@ -57,6 +110,23 @@ public class FocusableView: ParallaxView {
|
|
|
57
110
|
}
|
|
58
111
|
}
|
|
59
112
|
|
|
113
|
+
/// Register View in FocusableGroupManager
|
|
114
|
+
private func registerView() {
|
|
115
|
+
Task(priority: .userInitiated) {
|
|
116
|
+
guard itemId != nil,
|
|
117
|
+
groupId != nil,
|
|
118
|
+
isViewRegistered == false,
|
|
119
|
+
await FocusableGroupManager.shared.registerItem(self)
|
|
120
|
+
else {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await MainActor.run {
|
|
125
|
+
isViewRegistered = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
60
130
|
// TODO: Example of solution for future with groupId and focuse id
|
|
61
131
|
// func hintLeftFocusId() {
|
|
62
132
|
// if let onFocusLeft = dictFromReactNative,
|
|
@@ -77,12 +147,13 @@ public class FocusableView: ParallaxView {
|
|
|
77
147
|
return
|
|
78
148
|
}
|
|
79
149
|
|
|
80
|
-
|
|
81
|
-
|
|
150
|
+
Task {
|
|
151
|
+
await FocusableGroupManager.shared.unregisterItem(withId: itemId, inGroup: groupId)
|
|
152
|
+
}
|
|
82
153
|
}
|
|
83
154
|
|
|
84
|
-
func addFocusGuideIfNeeded(tag: NSNumber?,
|
|
85
|
-
|
|
155
|
+
private func addFocusGuideIfNeeded(tag: NSNumber?,
|
|
156
|
+
direction: UIRectEdge) {
|
|
86
157
|
if let tag,
|
|
87
158
|
let view = module?.viewForTag(tag: tag) {
|
|
88
159
|
_ = addFocusGuide(from: self,
|
|
@@ -92,7 +163,7 @@ public class FocusableView: ParallaxView {
|
|
|
92
163
|
}
|
|
93
164
|
}
|
|
94
165
|
|
|
95
|
-
func configureFocusLayout() {
|
|
166
|
+
private func configureFocusLayout() {
|
|
96
167
|
addFocusGuideIfNeeded(tag: nextTvosFocusLeft, direction: .left)
|
|
97
168
|
addFocusGuideIfNeeded(tag: nextTvosFocusRight, direction: .right)
|
|
98
169
|
addFocusGuideIfNeeded(tag: nextTvosFocusUp, direction: .top)
|
|
@@ -147,60 +218,18 @@ public class FocusableView: ParallaxView {
|
|
|
147
218
|
}
|
|
148
219
|
|
|
149
220
|
DispatchQueue.main.async { [weak self] in
|
|
150
|
-
FocusableGroupManager.updateFocus(self?.groupId,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
completion: nil)
|
|
221
|
+
FocusableGroupManager.shared.updateFocus(groupId: self?.groupId,
|
|
222
|
+
itemId: self?.itemId,
|
|
223
|
+
needsForceUpdate: true)
|
|
154
224
|
}
|
|
155
225
|
}
|
|
156
226
|
}
|
|
157
227
|
|
|
158
228
|
override public var canBecomeFocused: Bool {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
/// Define if view was registered
|
|
163
|
-
var isViewRegistered: Bool = false
|
|
164
|
-
|
|
165
|
-
override public init(frame: CGRect) {
|
|
166
|
-
super.init(frame: frame)
|
|
167
|
-
initialize()
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
public required init?(coder aDecoder: NSCoder) {
|
|
171
|
-
super.init(coder: aDecoder)
|
|
172
|
-
initialize()
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/// Initialize component
|
|
176
|
-
func initialize() {
|
|
177
|
-
delegate = self
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/// ID of the View provided by React-Native env
|
|
181
|
-
@objc public var itemId: String? {
|
|
182
|
-
didSet {
|
|
183
|
-
registerView()
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/// ID of the View provided by React-Native env
|
|
188
|
-
@objc public var groupId: String? {
|
|
189
|
-
didSet {
|
|
190
|
-
registerView()
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/// Register View in FocusableGroupManager
|
|
195
|
-
func registerView() {
|
|
196
|
-
guard itemId != nil,
|
|
197
|
-
groupId != nil,
|
|
198
|
-
isViewRegistered == false,
|
|
199
|
-
FocusableGroupManager.registerView(item: self)
|
|
200
|
-
else {
|
|
201
|
-
return
|
|
229
|
+
guard let focusableGroup else {
|
|
230
|
+
return focusable
|
|
202
231
|
}
|
|
203
232
|
|
|
204
|
-
|
|
233
|
+
return focusableGroup.canBacomeFocusable && focusable
|
|
205
234
|
}
|
|
206
235
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//
|
|
2
|
+
// FocusableGroupProtocol.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Anton Kononenko on 10/28/24.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
protocol FocusableGroupProtocol: AnyObject {
|
|
9
|
+
var canBacomeFocusable: Bool { get }
|
|
10
|
+
@MainActor func updatePreferredFocusEnv(with view: UIFocusEnvironment)
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/quick-brick-native-apple",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.5",
|
|
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"
|