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 CHANGED
@@ -1,6 +1,12 @@
1
- # ConcentricSheet
1
+ # Concentric Sheet
2
2
 
3
- Nitro-powered React Native `Modal` replacement for iOS sheet presentation.
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
- var didApply = false
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concentric-sheet",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "concentric-sheet",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -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
  }