@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "QuickBrickApple",
3
- "version": "6.9.4",
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.4"
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
- /// Storage for focusable groups view [GroupID:FocusableGroupViewInstance]
13
- var focusableGroups: [String: FocusableGroupView] = [:]
14
-
15
- /// Storage for focusable views [GroupID:[ViewID:FocusableViewIntance]]
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
- /// Register FocusableView at storage
21
- ///
22
- /// - Parameter item: FocusableView instance
23
- class func registerView(item: FocusableView) -> Bool {
24
- guard let itemId = item.itemId,
25
- let groupId = item.groupId
26
- else {
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
- newItemsGroup[itemId] = item
35
- itemsGroups[groupId] = newItemsGroup
36
- notifyGroupView(groupID: groupId)
42
+ updateService.sendFocusableGroupRegisteredUpdate(group)
37
43
  return true
38
44
  }
39
45
 
40
- class func unregisterFusableItem(itemId: String, groupId: String) {
41
- var newItemsGroup: [String: FocusableView] = [:]
42
- if let itemsGroup = itemsGroups[groupId] {
43
- newItemsGroup = itemsGroup
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
- newItemsGroup[itemId] = nil
47
- itemsGroups[groupId] = newItemsGroup
53
+ await notifyGroupView(groupId: groupId)
48
54
  }
49
55
 
50
- class func unregisterFusableGroup(itemId: String) {
51
- itemsGroups[itemId] = nil
52
- focusableGroups[itemId] = nil
56
+ public func getGroup(by groupID: String) async -> FocusableGroupView? {
57
+ await groupStorage.group(by: groupID)
53
58
  }
54
59
 
55
- /// Register FocusableGroup at storage
56
- ///
57
- /// - Parameter item: FocusableGroup instance
58
- class func registerFocusableGroup(group: FocusableGroupView) -> Bool {
59
- var retVal = false
60
+ // MARK: - Item Management
60
61
 
61
- if let id = group.itemId {
62
- focusableGroups[id] = group
63
- retVal = true
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 retVal
72
+ return success
66
73
  }
67
74
 
68
- /// Retrieve group by ID
69
- ///
70
- /// - Parameter groupID: ID of the group
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
- /// Retrieve All items that connected to Group
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
- /// Notify group view that item relevant to it was updated
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
- /// Retrieve FocusableView view instance
88
+ /// Notify group view that item relevant to it was updated
104
89
  ///
105
- /// - Parameters:
106
- /// - groupId: Id of the group
107
- /// - itemId: Id of the FocusableView to search
108
- /// - Returns: FocusableView instance if exist in searched group, otherwise nil
109
- class func item(byGroupId groupId: String,
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
- class func updateFocus(_ groupId: String?, itemId: String?, needsForceUpdate: Bool = false, completion: ((_ success: Bool) -> Void)? = nil) {
122
- DispatchQueue.main.async {
123
- if let groupId,
124
- let itemId,
125
- let groupView = focusableGroups[groupId],
126
- let viewToFocus = FocusableGroupManager.item(byGroupId: groupId,
127
- andItemId: itemId) {
128
- groupView.updatePrefferedFocusEnv(with: viewToFocus)
129
-
130
- var rootView: UIView? = groupView
131
- while rootView !== nil, rootView?.isReactRootView() == false {
132
- if let unwrapedRootView = rootView,
133
- let superView = unwrapedRootView.superview {
134
- rootView = superView
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
- if rootView == nil {
141
- return
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
- if let rootView,
145
- let superView = rootView.superview as? RCTRootView {
146
- superView.reactPreferredFocusedView = viewToFocus
147
- if needsForceUpdate == true {
148
- superView.setNeedsFocusUpdate()
149
- superView.updateFocusIfNeeded()
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
- itemId: itemId)
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
- itemId: itemId,
65
- needsForceUpdate: true, completion: { succeed in
66
- callback?([succeed])
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
+ }
@@ -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
- /// Completion that will be used when focus manager forcing to update focusable group
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
- private var isManuallyBlockingFocus: Bool = false
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
- _ = FocusableGroupManager.registerFocusableGroup(group: self)
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
- // Removing when react native releases the view
88
- FocusableGroupManager.unregisterFusableGroup(itemId: itemId)
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
- public func groupItemsUpdated(groupItems: [String: FocusableView]) {
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
- customPrefferedFocusEnvironment = [initialView]
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 = FocusableGroupManager.item(byGroupId: groupId,
125
- andItemId: initialItemId) {
126
- customPrefferedFocusEnvironment = [initialItemView]
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 = FocusableGroupManager.item(byGroupId: firstGroupId,
146
- andItemId: initialItemId)
203
+ let initialView = await manager.getItem(withId: firstGroupId,
204
+ inGroup: initialItemId)
147
205
  else {
148
206
  return false
149
207
  }
150
208
 
151
- customPrefferedFocusEnvironment = [initialView]
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 = FocusableGroupManager.itemsForGroup(by: groupId)
244
+ let groupItems = await manager.getItems(forGroup: groupId)
229
245
  if let initialItemView = groupItems[initialItemId] {
230
- customPrefferedFocusEnvironment = [initialItemView]
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: Any?) {
260
- guard let nextFocusView = nextFocuseItem as? UIView,
261
- nextFocusView.isDescendant(of: self)
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
- customPrefferedFocusEnvironment = [nextFocusView]
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
- // Ignoring focus events that blocks by native side for purpose (maybe changed if future)
331
- if isManuallyBlockingFocus == false {
332
- sendUpdateFocusEventToReactNative(bubbleEventBlock: onWillUpdateFocus,
333
- context: context)
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
- if resetFocusPrefferedEnvironmentIfNeeded(nextFocusedItem: context.nextFocusedItem) == false {
341
- updatePreferredFocusView(nextFocuseItem: context.nextFocusedItem)
345
+ Task(priority: .userInitiated) {
346
+ if await resetFocusPrefferedEnvironmentIfNeeded(nextFocusedItem: context.nextFocusedItem) == false {
347
+ updatePreferredFocusView(nextFocuseItem: context.nextFocusedItem)
348
+ }
342
349
  }
343
350
 
344
- sendUpdateFocusEventToReactNative(bubbleEventBlock: onWillUpdateFocus,
345
- context: context)
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
- sendUpdateFocusEventToReactNative(bubbleEventBlock: onDidUpdateFocus,
358
- context: context)
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
+ }
@@ -27,7 +27,7 @@ public class FocusableGroupViewModule: RCTViewManager {
27
27
 
28
28
  override public func view() -> UIView? {
29
29
  let retVal = FocusableGroupView()
30
- retVal.manager = self
30
+ retVal.setModule(self)
31
31
  return retVal
32
32
  }
33
33
 
@@ -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
- weak var module: FocusableViewModule?
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
- @objc public var preferredFocus: Bool = false {
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
- DispatchQueue.main.async { [weak self] in
41
- if let groupId = self?.groupId,
42
- let focusableGroup = focusableGroups[groupId] {
43
- // Update Prefered focus view in group
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
- // Removing when react native releases the view
81
- FocusableGroupManager.unregisterFusableItem(itemId: itemId, groupId: groupId)
150
+ Task {
151
+ await FocusableGroupManager.shared.unregisterItem(withId: itemId, inGroup: groupId)
152
+ }
82
153
  }
83
154
 
84
- func addFocusGuideIfNeeded(tag: NSNumber?,
85
- direction: UIRectEdge) {
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
- itemId: self?.itemId,
152
- needsForceUpdate: true,
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
- focusable
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
- isViewRegistered = true
233
+ return focusableGroup.canBacomeFocusable && focusable
205
234
  }
206
235
  }
@@ -27,7 +27,7 @@ public class FocusableViewModule: RCTViewManager {
27
27
 
28
28
  override public func view() -> UIView? {
29
29
  let view = FocusableView()
30
- view.module = self
30
+ view.setModule(self)
31
31
 
32
32
  return view
33
33
  }
@@ -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.4",
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"