@lodev09/react-native-true-sheet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +53 -0
- package/TrueSheet.podspec +42 -0
- package/android/build.gradle +94 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +17 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +20 -0
- package/ios/Extensions/UIView+pinTo.swift +25 -0
- package/ios/Extensions/UIViewController+detentForSize.swift +112 -0
- package/ios/TrueSheet-Bridging-Header.h +9 -0
- package/ios/TrueSheetView.swift +249 -0
- package/ios/TrueSheetViewController.swift +98 -0
- package/ios/TrueSheetViewManager.m +38 -0
- package/ios/TrueSheetViewManager.swift +46 -0
- package/ios/Utils/Logger.swift +43 -0
- package/ios/Utils/Promise.swift +25 -0
- package/lib/commonjs/TrueSheet.js +104 -0
- package/lib/commonjs/TrueSheet.js.map +1 -0
- package/lib/commonjs/TrueSheetModule.js +17 -0
- package/lib/commonjs/TrueSheetModule.js.map +1 -0
- package/lib/commonjs/index.js +28 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/TrueSheet.js +95 -0
- package/lib/module/TrueSheet.js.map +1 -0
- package/lib/module/TrueSheetModule.js +11 -0
- package/lib/module/TrueSheetModule.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/TrueSheet.d.ts +31 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetModule.d.ts +2 -0
- package/lib/typescript/src/TrueSheetModule.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +94 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +183 -0
- package/src/TrueSheet.tsx +162 -0
- package/src/TrueSheetModule.ts +18 -0
- package/src/index.ts +2 -0
- package/src/types.ts +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 lodev09
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# React Native True Sheet
|
|
2
|
+
|
|
3
|
+
The real native bottom sheet.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- ✅ Implemented on the native realm.
|
|
7
|
+
- ✅ **_NOT_** your pure JS, (re)animated View.
|
|
8
|
+
- ✅ Clean, fast and lightweight.
|
|
9
|
+
- ✅ Handles your Sscrolling needs, easy.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
yarn add @lodev09/react-native-true-sheet
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { TrueSheet } from "@lodev09/react-native-true-sheet";
|
|
21
|
+
|
|
22
|
+
// ...
|
|
23
|
+
|
|
24
|
+
const sheet = useRef<TrueSheet>(null)
|
|
25
|
+
|
|
26
|
+
const openSheet = () => {
|
|
27
|
+
sheet.current?.present()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View>
|
|
32
|
+
<Button onPress={openSheet} title="Open Sheet" />
|
|
33
|
+
<TrueSheet ref={sheet}>
|
|
34
|
+
// ...
|
|
35
|
+
</TrueSheet>
|
|
36
|
+
</View>
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
> TODO - laters
|
|
42
|
+
|
|
43
|
+
## Contributing
|
|
44
|
+
|
|
45
|
+
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
5
|
+
|
|
6
|
+
Pod::Spec.new do |s|
|
|
7
|
+
s.name = "TrueSheet"
|
|
8
|
+
s.version = package["version"]
|
|
9
|
+
s.summary = package["description"]
|
|
10
|
+
s.homepage = package["homepage"]
|
|
11
|
+
s.license = package["license"]
|
|
12
|
+
s.authors = package["author"]
|
|
13
|
+
|
|
14
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
15
|
+
s.source = { :git => "https://github.com/lodev09/react-native-true-sheet.git", :tag => "#{s.version}" }
|
|
16
|
+
|
|
17
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
18
|
+
|
|
19
|
+
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
20
|
+
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
21
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
22
|
+
install_modules_dependencies(s)
|
|
23
|
+
else
|
|
24
|
+
s.dependency "React-Core"
|
|
25
|
+
|
|
26
|
+
# Don't install the dependencies when we run `pod install` in the old architecture.
|
|
27
|
+
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
|
28
|
+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
|
29
|
+
s.pod_target_xcconfig = {
|
|
30
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
|
31
|
+
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
|
|
32
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
|
33
|
+
}
|
|
34
|
+
s.dependency "React-RCTFabric"
|
|
35
|
+
s.dependency "React-Codegen"
|
|
36
|
+
s.dependency "RCT-Folly"
|
|
37
|
+
s.dependency "RCTRequired"
|
|
38
|
+
s.dependency "RCTTypeSafety"
|
|
39
|
+
s.dependency "ReactCommon/turbomodule/core"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
|
|
3
|
+
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["TrueSheet_kotlinVersion"]
|
|
4
|
+
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
mavenCentral()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
dependencies {
|
|
11
|
+
classpath "com.android.tools.build:gradle:7.2.1"
|
|
12
|
+
// noinspection DifferentKotlinGradleVersion
|
|
13
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def isNewArchitectureEnabled() {
|
|
18
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
apply plugin: "com.android.library"
|
|
22
|
+
apply plugin: "kotlin-android"
|
|
23
|
+
|
|
24
|
+
if (isNewArchitectureEnabled()) {
|
|
25
|
+
apply plugin: "com.facebook.react"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def getExtOrDefault(name) {
|
|
29
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["TrueSheet_" + name]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def getExtOrIntegerDefault(name) {
|
|
33
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["TrueSheet_" + name]).toInteger()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def supportsNamespace() {
|
|
37
|
+
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
|
|
38
|
+
def major = parsed[0].toInteger()
|
|
39
|
+
def minor = parsed[1].toInteger()
|
|
40
|
+
|
|
41
|
+
// Namespace support was added in 7.3.0
|
|
42
|
+
return (major == 7 && minor >= 3) || major >= 8
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
android {
|
|
46
|
+
if (supportsNamespace()) {
|
|
47
|
+
namespace "com.lodev09.truesheet"
|
|
48
|
+
|
|
49
|
+
sourceSets {
|
|
50
|
+
main {
|
|
51
|
+
manifest.srcFile "src/main/AndroidManifestNew.xml"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
57
|
+
|
|
58
|
+
defaultConfig {
|
|
59
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
60
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
buildTypes {
|
|
65
|
+
release {
|
|
66
|
+
minifyEnabled false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lintOptions {
|
|
71
|
+
disable "GradleCompatible"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
compileOptions {
|
|
75
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
76
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
repositories {
|
|
81
|
+
mavenCentral()
|
|
82
|
+
google()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
86
|
+
|
|
87
|
+
dependencies {
|
|
88
|
+
// For < 0.71, this will be from the local maven repo
|
|
89
|
+
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
90
|
+
//noinspection GradleDynamicVersion
|
|
91
|
+
implementation "com.facebook.react:react-native:+"
|
|
92
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.lodev09.truesheet
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TrueSheetPackage : ReactPackage {
|
|
10
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
+
return emptyList()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
15
|
+
return listOf(TrueSheetViewManager())
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.lodev09.truesheet
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import android.view.View
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
|
|
9
|
+
class TrueSheetViewManager : SimpleViewManager<View>() {
|
|
10
|
+
override fun getName() = "TrueSheetView"
|
|
11
|
+
|
|
12
|
+
override fun createViewInstance(reactContext: ThemedReactContext): View {
|
|
13
|
+
return View(reactContext)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@ReactProp(name = "color")
|
|
17
|
+
fun setColor(view: View, color: String) {
|
|
18
|
+
view.setBackgroundColor(Color.parseColor(color))
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
extension UIView {
|
|
10
|
+
func pinTo(view: UIView, from edges: UIRectEdge = .all, with height: CGFloat? = nil) {
|
|
11
|
+
var constraints: [NSLayoutConstraint] = []
|
|
12
|
+
|
|
13
|
+
if edges.contains(.top) { constraints.append(topAnchor.constraint(equalTo: view.topAnchor)) }
|
|
14
|
+
if edges.contains(.bottom) { constraints.append(bottomAnchor.constraint(equalTo: view.bottomAnchor)) }
|
|
15
|
+
if edges.contains(.left) { constraints.append(leadingAnchor.constraint(equalTo: view.leadingAnchor)) }
|
|
16
|
+
if edges.contains(.right) { constraints.append(trailingAnchor.constraint(equalTo: view.trailingAnchor)) }
|
|
17
|
+
|
|
18
|
+
if let height { constraints.append(heightAnchor.constraint(equalToConstant: height)) }
|
|
19
|
+
|
|
20
|
+
if !constraints.isEmpty {
|
|
21
|
+
translatesAutoresizingMaskIntoConstraints = false
|
|
22
|
+
NSLayoutConstraint.activate(constraints)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
@available(iOS 15.0, *)
|
|
10
|
+
extension UISheetPresentationController.Detent.Identifier {
|
|
11
|
+
static let small = UISheetPresentationController.Detent.Identifier("small")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@available(iOS 15.0, *)
|
|
15
|
+
extension UIViewController {
|
|
16
|
+
private func detentFor(identifier: UISheetPresentationController.Detent.Identifier,
|
|
17
|
+
_ resolution: @escaping (CGFloat) -> Void) -> UISheetPresentationController.Detent {
|
|
18
|
+
switch identifier {
|
|
19
|
+
case .large:
|
|
20
|
+
if #available(iOS 16.0, *) {
|
|
21
|
+
return .custom(identifier: .large) { context in
|
|
22
|
+
let value = UISheetPresentationController.Detent.large().resolvedValue(in: context)
|
|
23
|
+
resolution(value ?? 0)
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
resolution(view.frame.height)
|
|
28
|
+
return .large()
|
|
29
|
+
}
|
|
30
|
+
case .medium:
|
|
31
|
+
if #available(iOS 16.0, *) {
|
|
32
|
+
return .custom(identifier: .medium) { context in
|
|
33
|
+
let value = UISheetPresentationController.Detent.medium().resolvedValue(in: context)
|
|
34
|
+
resolution(value ?? 0)
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
case .small:
|
|
39
|
+
if #available(iOS 16.0, *) {
|
|
40
|
+
return .custom { context in
|
|
41
|
+
let value = 0.25 * context.maximumDetentValue
|
|
42
|
+
resolution(value)
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resolution(view.frame.height / 2)
|
|
51
|
+
return .medium()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Get the custom detent based on the given size and view frame size
|
|
55
|
+
func detentFor(_ anySize: Any, with height: CGFloat?, _ resolution: @escaping (String, CGFloat) -> Void) -> UISheetPresentationController.Detent {
|
|
56
|
+
let id = "custom-\(anySize)"
|
|
57
|
+
|
|
58
|
+
if let floatSize = anySize as? CGFloat {
|
|
59
|
+
if #available(iOS 16.0, *) {
|
|
60
|
+
return .custom(identifier: identifier(from: id)) { context in
|
|
61
|
+
let value = min(floatSize, context.maximumDetentValue)
|
|
62
|
+
resolution(id, value)
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if var stringSize = anySize as? String {
|
|
69
|
+
switch stringSize {
|
|
70
|
+
case "small":
|
|
71
|
+
return detentFor(identifier: .small) { value in
|
|
72
|
+
resolution(UISheetPresentationController.Detent.Identifier.small.rawValue, value)
|
|
73
|
+
}
|
|
74
|
+
case "medium":
|
|
75
|
+
return detentFor(identifier: .medium) { value in
|
|
76
|
+
resolution(UISheetPresentationController.Detent.Identifier.medium.rawValue, value)
|
|
77
|
+
}
|
|
78
|
+
case "large":
|
|
79
|
+
return detentFor(identifier: .large) { value in
|
|
80
|
+
resolution(UISheetPresentationController.Detent.Identifier.large.rawValue, value)
|
|
81
|
+
}
|
|
82
|
+
case "auto":
|
|
83
|
+
if #available(iOS 16.0, *), let height {
|
|
84
|
+
return .custom(identifier: identifier(from: id)) { context in
|
|
85
|
+
let value = min(height, context.maximumDetentValue)
|
|
86
|
+
resolution(id, value)
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
if #available(iOS 16.0, *) {
|
|
92
|
+
// Percent
|
|
93
|
+
stringSize.removeAll(where: { $0 == "%" })
|
|
94
|
+
let floatSize = CGFloat((stringSize as NSString).floatValue)
|
|
95
|
+
if floatSize > 0.0 {
|
|
96
|
+
return .custom(identifier: identifier(from: id)) { context in
|
|
97
|
+
let value = min((floatSize / 100) * context.maximumDetentValue, context.maximumDetentValue)
|
|
98
|
+
resolution(id, value)
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return .medium()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func identifier(from id: String) -> UISheetPresentationController.Detent.Identifier {
|
|
110
|
+
return UISheetPresentationController.Detent.Identifier(id)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#import <React/RCTViewManager.h>
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
@objc(TrueSheetView)
|
|
10
|
+
class TrueSheetView: UIView, RCTInvalidating, TrueSheetViewControllerDelegate {
|
|
11
|
+
// MARK: - React properties
|
|
12
|
+
|
|
13
|
+
@objc var sizes: [Any] = []
|
|
14
|
+
|
|
15
|
+
// Events
|
|
16
|
+
@objc var onDismiss: RCTDirectEventBlock?
|
|
17
|
+
@objc var onPresent: RCTDirectEventBlock?
|
|
18
|
+
@objc var onSizeChange: RCTDirectEventBlock?
|
|
19
|
+
|
|
20
|
+
// MARK: - Private properties
|
|
21
|
+
|
|
22
|
+
private var isPresented = false
|
|
23
|
+
private var activeIndex: Int?
|
|
24
|
+
private var bridge: RCTBridge
|
|
25
|
+
private var touchHandler: RCTTouchHandler
|
|
26
|
+
private var viewController: TrueSheetViewController
|
|
27
|
+
|
|
28
|
+
// MARK: - Content properties
|
|
29
|
+
|
|
30
|
+
private var containerView: UIView?
|
|
31
|
+
|
|
32
|
+
private var contentView: UIView?
|
|
33
|
+
private var footerView: UIView?
|
|
34
|
+
private var rctScrollView: RCTScrollView?
|
|
35
|
+
|
|
36
|
+
private var isContentMounted: Bool {
|
|
37
|
+
return contentView != nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Content height minus the footer height for `auto` layout
|
|
41
|
+
private var contentHeight: CGFloat {
|
|
42
|
+
guard let contentView else { return 0 }
|
|
43
|
+
|
|
44
|
+
// Exclude bottom safe area for consistency with a Scrollable content
|
|
45
|
+
let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
|
|
46
|
+
let bottomInset = window?.safeAreaInsets.bottom ?? 0
|
|
47
|
+
|
|
48
|
+
return contentView.frame.height - bottomInset
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Setup
|
|
52
|
+
|
|
53
|
+
init(with bridge: RCTBridge) {
|
|
54
|
+
self.bridge = bridge
|
|
55
|
+
|
|
56
|
+
viewController = TrueSheetViewController()
|
|
57
|
+
touchHandler = RCTTouchHandler(bridge: bridge)
|
|
58
|
+
|
|
59
|
+
super.init(frame: .zero)
|
|
60
|
+
|
|
61
|
+
viewController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
|
|
62
|
+
viewController.delegate = self
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@available(*, unavailable)
|
|
66
|
+
required init?(coder _: NSCoder) {
|
|
67
|
+
fatalError("init(coder:) has not been implemented")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func viewDidChangeWidth(_ width: CGFloat) {
|
|
71
|
+
guard let containerView else { return }
|
|
72
|
+
|
|
73
|
+
let size = CGSize(width: width, height: containerView.bounds.height)
|
|
74
|
+
bridge.uiManager.setSize(size, for: containerView)
|
|
75
|
+
|
|
76
|
+
if let footerView {
|
|
77
|
+
bridge.uiManager.setSize(size, for: footerView)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func didDismiss() {
|
|
82
|
+
isPresented = false
|
|
83
|
+
activeIndex = nil
|
|
84
|
+
|
|
85
|
+
onDismiss?(nil)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func didChangeSize(_ value: CGFloat, at index: Int) {
|
|
89
|
+
if index != activeIndex {
|
|
90
|
+
activeIndex = index
|
|
91
|
+
onSizeChange?(["index": index, "value": value])
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override func insertReactSubview(_ subview: UIView!, at index: Int) {
|
|
96
|
+
super.insertReactSubview(subview, at: index)
|
|
97
|
+
|
|
98
|
+
guard containerView == nil else {
|
|
99
|
+
Logger.error("Sheet can only have one content view.")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
viewController.view.insertSubview(subview, at: 0)
|
|
104
|
+
viewController.view.backgroundColor = backgroundColor ?? .white
|
|
105
|
+
backgroundColor = .clear
|
|
106
|
+
|
|
107
|
+
containerView = subview
|
|
108
|
+
touchHandler.attach(to: subview)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override func removeReactSubview(_ subview: UIView!) {
|
|
112
|
+
guard subview == containerView else {
|
|
113
|
+
Logger.error("Cannot remove view other than sheet view")
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
super.removeReactSubview(subview)
|
|
118
|
+
|
|
119
|
+
touchHandler.detach(from: subview)
|
|
120
|
+
|
|
121
|
+
containerView = nil
|
|
122
|
+
contentView = nil
|
|
123
|
+
footerView = nil
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override func didUpdateReactSubviews() {
|
|
127
|
+
// Do nothing, as subviews are managed by `insertReactSubview`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override func layoutSubviews() {
|
|
131
|
+
super.layoutSubviews()
|
|
132
|
+
|
|
133
|
+
if let containerView, contentView == nil {
|
|
134
|
+
contentView = containerView.subviews.first
|
|
135
|
+
setupContentIfNeeded()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - Prop setters
|
|
140
|
+
|
|
141
|
+
@objc
|
|
142
|
+
func setScrollableHandle(_ tag: NSNumber?) {
|
|
143
|
+
let view = bridge.uiManager.view(forReactTag: tag) as? RCTScrollView
|
|
144
|
+
rctScrollView = view
|
|
145
|
+
setupContentIfNeeded()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@objc
|
|
149
|
+
func setFooterHandle(_ tag: NSNumber?) {
|
|
150
|
+
let view = bridge.uiManager.view(forReactTag: tag)
|
|
151
|
+
footerView = view
|
|
152
|
+
setupContentIfNeeded()
|
|
153
|
+
|
|
154
|
+
if #available(iOS 16.0, *) {
|
|
155
|
+
viewController.sheet?.invalidateDetents()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func invalidate() {
|
|
160
|
+
viewController.dismiss(animated: true)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// MARK: - Methods
|
|
164
|
+
|
|
165
|
+
func setupContentIfNeeded() {
|
|
166
|
+
guard isContentMounted, let containerView else { return }
|
|
167
|
+
|
|
168
|
+
containerView.pinTo(view: viewController.view)
|
|
169
|
+
|
|
170
|
+
// Add constraints to fix weirdness and support ScrollView
|
|
171
|
+
if let contentView, let rctScrollView {
|
|
172
|
+
contentView.pinTo(view: containerView)
|
|
173
|
+
rctScrollView.pinTo(view: contentView)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Pin footer at the bottom
|
|
177
|
+
if let footerView {
|
|
178
|
+
containerView.bringSubviewToFront(footerView)
|
|
179
|
+
footerView.pinTo(
|
|
180
|
+
view: viewController.view,
|
|
181
|
+
from: [.bottom, .left, .right],
|
|
182
|
+
with: footerView.frame.height
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func dismiss(promise: Promise) {
|
|
188
|
+
if isPresented {
|
|
189
|
+
viewController.dismiss(animated: true) {
|
|
190
|
+
promise.resolve(true)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func present(at index: Int, promise: Promise) {
|
|
196
|
+
let rvc = reactViewController()
|
|
197
|
+
|
|
198
|
+
guard let rvc else {
|
|
199
|
+
promise.reject(message: "No react view controller present.")
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if #available(iOS 15.0, *), let sheet = viewController.sheet {
|
|
204
|
+
viewController.configureSheet(for: sizes, with: contentHeight)
|
|
205
|
+
|
|
206
|
+
guard sizes.indices.contains(index) else {
|
|
207
|
+
promise.reject(message: "Size at \(index) is not configured.")
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var identifier: UISheetPresentationController.Detent.Identifier = .medium
|
|
212
|
+
|
|
213
|
+
if sheet.detents.indices.contains(index) {
|
|
214
|
+
let detent = sheet.detents[index]
|
|
215
|
+
if #available(iOS 16.0, *) {
|
|
216
|
+
identifier = detent.identifier
|
|
217
|
+
} else if detent == .large() {
|
|
218
|
+
identifier = .large
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if isPresented {
|
|
223
|
+
sheet.animateChanges {
|
|
224
|
+
sheet.selectedDetentIdentifier = identifier
|
|
225
|
+
|
|
226
|
+
// Notify when size is changed programatically
|
|
227
|
+
let info = viewController.detentValues.first(where: { $0.value.index == index })
|
|
228
|
+
if let sizeValue = info?.value.value {
|
|
229
|
+
self.didChangeSize(sizeValue, at: index)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
sheet.selectedDetentIdentifier = identifier
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if !isPresented {
|
|
238
|
+
// Keep track of the active index
|
|
239
|
+
activeIndex = index
|
|
240
|
+
|
|
241
|
+
rvc.present(viewController, animated: true) {
|
|
242
|
+
self.isPresented = true
|
|
243
|
+
self.onPresent?(nil)
|
|
244
|
+
|
|
245
|
+
promise.resolve(true)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|