concentric-sheet 1.0.0 → 1.0.1
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.
Potentially problematic release.
This version of concentric-sheet might be problematic. Click here for more details.
- package/README.md +8 -2
- package/ios/SheetModalController.swift +137 -49
- package/lib/NativeSheetModal.js +8 -0
- package/lib/specs/SheetModalController.nitro.d.ts +1 -0
- package/nitrogen/generated/ios/c++/HybridSheetModalControllerSpecSwift.hpp +8 -0
- package/nitrogen/generated/ios/swift/HybridSheetModalControllerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridSheetModalControllerSpec_cxx.swift +12 -0
- package/nitrogen/generated/shared/c++/HybridSheetModalControllerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridSheetModalControllerSpec.hpp +1 -0
- package/package.json +1 -1
- package/src/NativeSheetModal.tsx +7 -0
- package/src/specs/SheetModalController.nitro.ts +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Concentric Sheet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
https://github.com/user-attachments/assets/df69aee2-a0cc-477c-9425-aee1dacd7f1d
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
React Native (iOS only) `Modal` replacement for iOS sheet presentation.
|
|
4
10
|
|
|
5
11
|
This package keeps React Native's `Modal` behavior, and adds runtime access to
|
|
6
12
|
native `UIViewController` / `UISheetPresentationController` options such as:
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import ObjectiveC.runtime
|
|
2
3
|
import UIKit
|
|
3
4
|
|
|
4
5
|
public final class SheetModalController: HybridSheetModalControllerSpec {
|
|
6
|
+
private static var didInstallModalHostHooks = false
|
|
7
|
+
private static weak var sharedInstance: SheetModalController?
|
|
8
|
+
private static var cachedPresentedModalConfig: PresentedModalConfig?
|
|
9
|
+
|
|
5
10
|
public override init() {
|
|
6
11
|
super.init()
|
|
12
|
+
Self.sharedInstance = self
|
|
13
|
+
Self.installModalHostHooksIfNeeded()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public func cachePresentedModalConfig(config: PresentedModalConfig) throws -> Bool {
|
|
17
|
+
return try onMainThread {
|
|
18
|
+
Self.cachedPresentedModalConfig = config
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
7
21
|
}
|
|
8
22
|
|
|
9
23
|
public func applyPresentedModalConfig(config: PresentedModalConfig) throws -> Bool {
|
|
@@ -12,55 +26,7 @@ public final class SheetModalController: HybridSheetModalControllerSpec {
|
|
|
12
26
|
return false
|
|
13
27
|
}
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
if let isModalInPresentation = config.isModalInPresentation {
|
|
17
|
-
controller.isModalInPresentation = isModalInPresentation
|
|
18
|
-
didApply = true
|
|
19
|
-
}
|
|
20
|
-
if config.preferredContentWidth != nil || config.preferredContentHeight != nil {
|
|
21
|
-
var preferredContentSize = controller.preferredContentSize
|
|
22
|
-
if let width = config.preferredContentWidth {
|
|
23
|
-
preferredContentSize.width = CGFloat(width)
|
|
24
|
-
}
|
|
25
|
-
if let height = config.preferredContentHeight {
|
|
26
|
-
preferredContentSize.height = CGFloat(height)
|
|
27
|
-
}
|
|
28
|
-
controller.preferredContentSize = preferredContentSize
|
|
29
|
-
didApply = true
|
|
30
|
-
}
|
|
31
|
-
if let modalViewBackground = config.modalViewBackground {
|
|
32
|
-
applyModalViewBackground(modalViewBackground, to: controller)
|
|
33
|
-
didApply = true
|
|
34
|
-
}
|
|
35
|
-
if let cornerConfiguration = config.cornerConfiguration {
|
|
36
|
-
if #available(iOS 26.0, *) {
|
|
37
|
-
let didFullyApply = applyCornerConfiguration(cornerConfiguration, to: controller)
|
|
38
|
-
didApply = didApply || didFullyApply
|
|
39
|
-
} else {
|
|
40
|
-
didApply = true
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if let sheetConfig = config.sheet {
|
|
45
|
-
if #available(iOS 15.0, *), let sheet = controller.sheetPresentationController {
|
|
46
|
-
applySheetConfig(sheetConfig, to: sheet)
|
|
47
|
-
if #available(iOS 26.0, *),
|
|
48
|
-
let cornerConfiguration = config.cornerConfiguration
|
|
49
|
-
{
|
|
50
|
-
// Corner configuration is authoritative when provided.
|
|
51
|
-
// For concentric/capsule we clear preferredCornerRadius so UIKit can
|
|
52
|
-
// use its own sheet geometry instead of a forced fixed radius.
|
|
53
|
-
sheet.preferredCornerRadius = derivedSheetCornerRadius(from: cornerConfiguration)
|
|
54
|
-
} else if sheetConfig.preferredCornerRadius == nil {
|
|
55
|
-
sheet.preferredCornerRadius = nil
|
|
56
|
-
}
|
|
57
|
-
didApply = true
|
|
58
|
-
} else {
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return didApply || config.sheet == nil
|
|
29
|
+
return applyConfig(config, to: controller)
|
|
64
30
|
}
|
|
65
31
|
}
|
|
66
32
|
|
|
@@ -77,6 +43,58 @@ public final class SheetModalController: HybridSheetModalControllerSpec {
|
|
|
77
43
|
}
|
|
78
44
|
}
|
|
79
45
|
|
|
46
|
+
private func applyConfig(_ config: PresentedModalConfig, to controller: UIViewController) -> Bool {
|
|
47
|
+
var didApply = false
|
|
48
|
+
if let isModalInPresentation = config.isModalInPresentation {
|
|
49
|
+
controller.isModalInPresentation = isModalInPresentation
|
|
50
|
+
didApply = true
|
|
51
|
+
}
|
|
52
|
+
if config.preferredContentWidth != nil || config.preferredContentHeight != nil {
|
|
53
|
+
var preferredContentSize = controller.preferredContentSize
|
|
54
|
+
if let width = config.preferredContentWidth {
|
|
55
|
+
preferredContentSize.width = CGFloat(width)
|
|
56
|
+
}
|
|
57
|
+
if let height = config.preferredContentHeight {
|
|
58
|
+
preferredContentSize.height = CGFloat(height)
|
|
59
|
+
}
|
|
60
|
+
controller.preferredContentSize = preferredContentSize
|
|
61
|
+
didApply = true
|
|
62
|
+
}
|
|
63
|
+
if let modalViewBackground = config.modalViewBackground {
|
|
64
|
+
applyModalViewBackground(modalViewBackground, to: controller)
|
|
65
|
+
didApply = true
|
|
66
|
+
}
|
|
67
|
+
if let cornerConfiguration = config.cornerConfiguration {
|
|
68
|
+
if #available(iOS 26.0, *) {
|
|
69
|
+
let didFullyApply = applyCornerConfiguration(cornerConfiguration, to: controller)
|
|
70
|
+
didApply = didApply || didFullyApply
|
|
71
|
+
} else {
|
|
72
|
+
didApply = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if let sheetConfig = config.sheet {
|
|
77
|
+
if #available(iOS 15.0, *), let sheet = controller.sheetPresentationController {
|
|
78
|
+
applySheetConfig(sheetConfig, to: sheet)
|
|
79
|
+
if #available(iOS 26.0, *),
|
|
80
|
+
let cornerConfiguration = config.cornerConfiguration
|
|
81
|
+
{
|
|
82
|
+
// Corner configuration is authoritative when provided.
|
|
83
|
+
// For concentric/capsule we clear preferredCornerRadius so UIKit can
|
|
84
|
+
// use its own sheet geometry instead of a forced fixed radius.
|
|
85
|
+
sheet.preferredCornerRadius = derivedSheetCornerRadius(from: cornerConfiguration)
|
|
86
|
+
} else if sheetConfig.preferredCornerRadius == nil {
|
|
87
|
+
sheet.preferredCornerRadius = nil
|
|
88
|
+
}
|
|
89
|
+
didApply = true
|
|
90
|
+
} else {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return didApply || config.sheet == nil
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
@available(iOS 15.0, *)
|
|
81
99
|
private func applySheetConfig(
|
|
82
100
|
_ config: SheetPresentationConfig,
|
|
@@ -291,4 +309,74 @@ public final class SheetModalController: HybridSheetModalControllerSpec {
|
|
|
291
309
|
|
|
292
310
|
return nil
|
|
293
311
|
}
|
|
312
|
+
|
|
313
|
+
// TODO: super hacky find better solution 🤷♂️
|
|
314
|
+
@MainActor
|
|
315
|
+
fileprivate static func preconfigurePresentedControllerIfNeeded(_ controller: UIViewController) {
|
|
316
|
+
guard isReactNativeModalHostController(controller) else { return }
|
|
317
|
+
guard let config = cachedPresentedModalConfig else { return }
|
|
318
|
+
_ = sharedInstance?.applyConfig(config, to: controller)
|
|
319
|
+
// One-shot consume so this config cannot leak to unrelated RN modals.
|
|
320
|
+
cachedPresentedModalConfig = nil
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// TODO: super hacky find better solution 🤷♂️
|
|
324
|
+
private static func isReactNativeModalHostController(_ controller: UIViewController) -> Bool {
|
|
325
|
+
let className = NSStringFromClass(type(of: controller))
|
|
326
|
+
return className.contains("ModalHostViewController")
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// TODO: super hacky find better solution 🤷♂️
|
|
330
|
+
private static func installModalHostHooksIfNeeded() {
|
|
331
|
+
guard !didInstallModalHostHooks else { return }
|
|
332
|
+
didInstallModalHostHooks = true
|
|
333
|
+
|
|
334
|
+
let classNames = ["RCTFabricModalHostViewController", "RCTModalHostViewController"]
|
|
335
|
+
for className in classNames {
|
|
336
|
+
guard let cls = NSClassFromString(className) else { continue }
|
|
337
|
+
swizzleViewWillAppear(on: cls)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// TODO: super hacky find better solution 🤷♂️
|
|
342
|
+
private static func swizzleViewWillAppear(on cls: AnyClass) {
|
|
343
|
+
let originalSelector = #selector(UIViewController.viewWillAppear(_:))
|
|
344
|
+
let swizzledSelector = #selector(UIViewController.ncs_modalHost_viewWillAppear(_:))
|
|
345
|
+
|
|
346
|
+
guard
|
|
347
|
+
let originalMethod = class_getInstanceMethod(cls, originalSelector),
|
|
348
|
+
let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)
|
|
349
|
+
else {
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let didAddMethod = class_addMethod(
|
|
354
|
+
cls,
|
|
355
|
+
swizzledSelector,
|
|
356
|
+
method_getImplementation(swizzledMethod),
|
|
357
|
+
method_getTypeEncoding(swizzledMethod)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
guard didAddMethod,
|
|
361
|
+
let classSwizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
|
|
362
|
+
else {
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
method_exchangeImplementations(originalMethod, classSwizzledMethod)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private extension UIViewController {
|
|
371
|
+
@objc
|
|
372
|
+
func ncs_modalHost_viewWillAppear(_ animated: Bool) {
|
|
373
|
+
if Thread.isMainThread {
|
|
374
|
+
SheetModalController.preconfigurePresentedControllerIfNeeded(self)
|
|
375
|
+
} else {
|
|
376
|
+
DispatchQueue.main.sync {
|
|
377
|
+
SheetModalController.preconfigurePresentedControllerIfNeeded(self)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
ncs_modalHost_viewWillAppear(animated)
|
|
381
|
+
}
|
|
294
382
|
}
|
package/lib/NativeSheetModal.js
CHANGED
|
@@ -111,6 +111,14 @@ export function Modal(props) {
|
|
|
111
111
|
};
|
|
112
112
|
attempt();
|
|
113
113
|
}, [clearRetryTimeout, nativeConfig, visible]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (Platform.OS !== 'ios')
|
|
116
|
+
return;
|
|
117
|
+
const controller = getSheetModalController();
|
|
118
|
+
if (controller == null)
|
|
119
|
+
return;
|
|
120
|
+
controller.cachePresentedModalConfig(nativeConfig);
|
|
121
|
+
}, [nativeConfig, visible]);
|
|
114
122
|
useEffect(() => {
|
|
115
123
|
applyNativeConfigWithRetry();
|
|
116
124
|
}, [applyNativeConfigWithRetry]);
|
|
@@ -29,6 +29,7 @@ export interface PresentedModalConfig {
|
|
|
29
29
|
export interface SheetModalController extends HybridObject<{
|
|
30
30
|
ios: 'swift';
|
|
31
31
|
}> {
|
|
32
|
+
cachePresentedModalConfig(config: PresentedModalConfig): boolean;
|
|
32
33
|
applyPresentedModalConfig(config: PresentedModalConfig): boolean;
|
|
33
34
|
dismissPresentedModal(animated: boolean): boolean;
|
|
34
35
|
}
|
|
@@ -84,6 +84,14 @@ namespace margelo::nitro::concentricsheet {
|
|
|
84
84
|
|
|
85
85
|
public:
|
|
86
86
|
// Methods
|
|
87
|
+
inline bool cachePresentedModalConfig(const PresentedModalConfig& config) override {
|
|
88
|
+
auto __result = _swiftPart.cachePresentedModalConfig(std::forward<decltype(config)>(config));
|
|
89
|
+
if (__result.hasError()) [[unlikely]] {
|
|
90
|
+
std::rethrow_exception(__result.error());
|
|
91
|
+
}
|
|
92
|
+
auto __value = std::move(__result.value());
|
|
93
|
+
return __value;
|
|
94
|
+
}
|
|
87
95
|
inline bool applyPresentedModalConfig(const PresentedModalConfig& config) override {
|
|
88
96
|
auto __result = _swiftPart.applyPresentedModalConfig(std::forward<decltype(config)>(config));
|
|
89
97
|
if (__result.hasError()) [[unlikely]] {
|
|
@@ -13,6 +13,7 @@ public protocol HybridSheetModalControllerSpec_protocol: HybridObject {
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
// Methods
|
|
16
|
+
func cachePresentedModalConfig(config: PresentedModalConfig) throws -> Bool
|
|
16
17
|
func applyPresentedModalConfig(config: PresentedModalConfig) throws -> Bool
|
|
17
18
|
func dismissPresentedModal(animated: Bool) throws -> Bool
|
|
18
19
|
}
|
|
@@ -124,6 +124,18 @@ open class HybridSheetModalControllerSpec_cxx {
|
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
// Methods
|
|
127
|
+
@inline(__always)
|
|
128
|
+
public final func cachePresentedModalConfig(config: PresentedModalConfig) -> bridge.Result_bool_ {
|
|
129
|
+
do {
|
|
130
|
+
let __result = try self.__implementation.cachePresentedModalConfig(config: config)
|
|
131
|
+
let __resultCpp = __result
|
|
132
|
+
return bridge.create_Result_bool_(__resultCpp)
|
|
133
|
+
} catch (let __error) {
|
|
134
|
+
let __exceptionPtr = __error.toCpp()
|
|
135
|
+
return bridge.create_Result_bool_(__exceptionPtr)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
@inline(__always)
|
|
128
140
|
public final func applyPresentedModalConfig(config: PresentedModalConfig) -> bridge.Result_bool_ {
|
|
129
141
|
do {
|
|
@@ -14,6 +14,7 @@ namespace margelo::nitro::concentricsheet {
|
|
|
14
14
|
HybridObject::loadHybridMethods();
|
|
15
15
|
// load custom methods/properties
|
|
16
16
|
registerHybrids(this, [](Prototype& prototype) {
|
|
17
|
+
prototype.registerHybridMethod("cachePresentedModalConfig", &HybridSheetModalControllerSpec::cachePresentedModalConfig);
|
|
17
18
|
prototype.registerHybridMethod("applyPresentedModalConfig", &HybridSheetModalControllerSpec::applyPresentedModalConfig);
|
|
18
19
|
prototype.registerHybridMethod("dismissPresentedModal", &HybridSheetModalControllerSpec::dismissPresentedModal);
|
|
19
20
|
});
|
|
@@ -49,6 +49,7 @@ namespace margelo::nitro::concentricsheet {
|
|
|
49
49
|
|
|
50
50
|
public:
|
|
51
51
|
// Methods
|
|
52
|
+
virtual bool cachePresentedModalConfig(const PresentedModalConfig& config) = 0;
|
|
52
53
|
virtual bool applyPresentedModalConfig(const PresentedModalConfig& config) = 0;
|
|
53
54
|
virtual bool dismissPresentedModal(bool animated) = 0;
|
|
54
55
|
|
package/package.json
CHANGED
package/src/NativeSheetModal.tsx
CHANGED
|
@@ -173,6 +173,13 @@ export function Modal(props: NativeSheetModalProps) {
|
|
|
173
173
|
attempt()
|
|
174
174
|
}, [clearRetryTimeout, nativeConfig, visible])
|
|
175
175
|
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (Platform.OS !== 'ios') return
|
|
178
|
+
const controller = getSheetModalController()
|
|
179
|
+
if (controller == null) return
|
|
180
|
+
controller.cachePresentedModalConfig(nativeConfig)
|
|
181
|
+
}, [nativeConfig, visible])
|
|
182
|
+
|
|
176
183
|
useEffect(() => {
|
|
177
184
|
applyNativeConfigWithRetry()
|
|
178
185
|
}, [applyNativeConfigWithRetry])
|
|
@@ -36,6 +36,7 @@ export interface PresentedModalConfig {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface SheetModalController extends HybridObject<{ ios: 'swift' }> {
|
|
39
|
+
cachePresentedModalConfig(config: PresentedModalConfig): boolean
|
|
39
40
|
applyPresentedModalConfig(config: PresentedModalConfig): boolean
|
|
40
41
|
dismissPresentedModal(animated: boolean): boolean
|
|
41
42
|
}
|