@granite-js/screen 1.0.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/GraniteScreen.podspec +25 -0
  3. package/LICENSE +202 -0
  4. package/android/CMakeLists.txt +62 -0
  5. package/android/build.gradle +63 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/BundleEvaluator.cpp +27 -0
  8. package/android/src/main/cpp/BundleEvaluator.h +17 -0
  9. package/android/src/main/cpp/onLoad.cpp +6 -0
  10. package/android/src/main/kotlin/run/granite/BundleEvaluator.kt +50 -0
  11. package/android/src/main/kotlin/run/granite/BundleLoader.kt +40 -0
  12. package/android/src/main/kotlin/run/granite/DefaultBundleLoader.kt +20 -0
  13. package/android/src/main/kotlin/run/granite/DefaultErrorView.kt +58 -0
  14. package/android/src/main/kotlin/run/granite/DefaultLoadingView.kt +51 -0
  15. package/android/src/main/kotlin/run/granite/GraniteReactDelegate.kt +76 -0
  16. package/android/src/main/kotlin/run/granite/GraniteReactDelegateImpl.kt +448 -0
  17. package/android/src/main/kotlin/run/granite/GraniteReactHost.kt +113 -0
  18. package/android/src/main/kotlin/run/granite/ReactHostFactory.kt +106 -0
  19. package/gradle-plugin/LICENSE +201 -0
  20. package/gradle-plugin/README.md +578 -0
  21. package/gradle-plugin/build.gradle.kts +97 -0
  22. package/gradle-plugin/gradle/libs.versions.toml +17 -0
  23. package/gradle-plugin/gradle/wrapper/gradle-wrapper.jar +0 -0
  24. package/gradle-plugin/gradle/wrapper/gradle-wrapper.properties +7 -0
  25. package/gradle-plugin/gradle.properties +12 -0
  26. package/gradle-plugin/gradlew +248 -0
  27. package/gradle-plugin/gradlew.bat +93 -0
  28. package/gradle-plugin/settings.gradle.kts +1 -0
  29. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteExtension.kt +225 -0
  30. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GranitePlugin.kt +784 -0
  31. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteRootExtension.kt +107 -0
  32. package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteRootProjectPlugin.kt +290 -0
  33. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/BuildConfigConfigurator.kt +69 -0
  34. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DependencyConfigurator.kt +232 -0
  35. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DependencyCoordinates.kt +29 -0
  36. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DevServerResourceConfigurator.kt +101 -0
  37. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/JniPackagingConfigurator.kt +160 -0
  38. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/NdkConfigurator.kt +135 -0
  39. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/RepositoryConfigurator.kt +148 -0
  40. package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/ResourceConfigurator.kt +56 -0
  41. package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/CMakeGenerator.kt +105 -0
  42. package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/CppAutolinkingGenerator.kt +152 -0
  43. package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/EntryPointGenerator.kt +100 -0
  44. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/AndroidDependencyConfig.kt +23 -0
  45. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/AutolinkingConfig.kt +89 -0
  46. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/CMakeEntry.kt +47 -0
  47. package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/NativeModule.kt +177 -0
  48. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/AssetPackagingTask.kt +194 -0
  49. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/AutolinkingTask.kt +431 -0
  50. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/BundleTask.kt +275 -0
  51. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/CodegenArtifactsTask.kt +218 -0
  52. package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/CodegenSchemaTask.kt +186 -0
  53. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/AutolinkingParser.kt +128 -0
  54. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/ConflictDetector.kt +121 -0
  55. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/JdkValidator.kt +73 -0
  56. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/NodeExecutableFinder.kt +43 -0
  57. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/ReactNativeVersionReader.kt +329 -0
  58. package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/TaskDependencyValidator.kt +198 -0
  59. package/gradle-plugin/src/test/kotlin/run/granite/gradle/GraniteExtensionTest.kt +191 -0
  60. package/gradle-plugin/src/test/kotlin/run/granite/gradle/GranitePluginTest.kt +156 -0
  61. package/gradle-plugin/src/test/kotlin/run/granite/gradle/GraniteRootProjectPluginTest.kt +87 -0
  62. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/BuildConfigConfiguratorTest.kt +115 -0
  63. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/DependencyConfiguratorTest.kt +338 -0
  64. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/DevServerResourceConfiguratorTest.kt +205 -0
  65. package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/ResourceConfiguratorTest.kt +131 -0
  66. package/gradle-plugin/src/test/kotlin/run/granite/gradle/fixtures/NativeModuleFixtures.kt +67 -0
  67. package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/CMakeGeneratorTest.kt +71 -0
  68. package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/CppAutolinkingGeneratorTest.kt +344 -0
  69. package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/EntryPointGeneratorTest.kt +40 -0
  70. package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/AutolinkingConfigTest.kt +350 -0
  71. package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/CMakeEntryTest.kt +200 -0
  72. package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/NativeModuleTest.kt +562 -0
  73. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/AssetPackagingTaskTest.kt +318 -0
  74. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/AutolinkingTaskTest.kt +89 -0
  75. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/BundleTaskTest.kt +68 -0
  76. package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/CodegenTasksTest.kt +410 -0
  77. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/AutolinkingParserTest.kt +335 -0
  78. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/ConflictDetectorTest.kt +75 -0
  79. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/JdkValidatorTest.kt +88 -0
  80. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/ReactNativeVersionReaderTest.kt +585 -0
  81. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/TaskDependencyValidatorTest.kt +123 -0
  82. package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/TaskTestUtils.kt +88 -0
  83. package/gradle-plugin/src/test/resources/fixtures/sample-rn-config.json +45 -0
  84. package/ios/BundleLoader/BundleEvaluator.h +16 -0
  85. package/ios/BundleLoader/BundleEvaluator.mm +76 -0
  86. package/ios/BundleLoader/BundleLoadable.swift +91 -0
  87. package/ios/GraniteBundleLoaderTypes.swift +7 -0
  88. package/ios/GraniteScreen.h +12 -0
  89. package/ios/ReactNativeHosting/DefaultViews.swift +138 -0
  90. package/ios/ReactNativeHosting/GraniteDefaultModuleProvider.h +24 -0
  91. package/ios/ReactNativeHosting/GraniteDefaultModuleProvider.mm +22 -0
  92. package/ios/ReactNativeHosting/GraniteHostingHelper.swift +103 -0
  93. package/ios/ReactNativeHosting/GraniteNativeFactory.swift +35 -0
  94. package/ios/ReactNativeHosting/GraniteNativeFactoryDelegateImpl.swift +30 -0
  95. package/ios/ReactNativeHosting/GraniteNativeFactoryImpl.swift +24 -0
  96. package/ios/ReactNativeHosting/GraniteReactHost.swift +39 -0
  97. package/ios/ReactNativeHosting/GraniteScreen-Bridging-Header.h +12 -0
  98. package/package.json +59 -0
  99. package/react-native.config.js +8 -0
@@ -0,0 +1,88 @@
1
+ package run.granite.gradle.utils
2
+
3
+ import org.gradle.api.Project
4
+ import org.gradle.api.Task
5
+ import org.gradle.testfixtures.ProjectBuilder
6
+ import java.io.File
7
+
8
+ /**
9
+ * Test utilities for creating and configuring Gradle tasks in tests.
10
+ */
11
+
12
+ /**
13
+ * Creates a test project with Android Library plugin applied.
14
+ *
15
+ * @param projectDir Optional project directory (uses temp dir if not specified)
16
+ * @return Configured Gradle project
17
+ */
18
+ internal fun createProject(projectDir: File? = null): Project {
19
+ val project = ProjectBuilder.builder()
20
+ .apply {
21
+ if (projectDir != null) {
22
+ withProjectDir(projectDir)
23
+ }
24
+ }
25
+ .build()
26
+
27
+ // Apply Android Library plugin for testing
28
+ project.plugins.apply("com.android.library")
29
+
30
+ return project
31
+ }
32
+
33
+ /**
34
+ * Creates and registers a test task of the specified type.
35
+ *
36
+ * This helper allows testing of abstract Gradle tasks by using Gradle's
37
+ * task registration mechanism which handles property injection.
38
+ *
39
+ * @param T Task type to create
40
+ * @param project Project to register the task in (defaults to new test project)
41
+ * @param taskName Name for the task (defaults to class simple name)
42
+ * @param block Configuration block to setup task properties
43
+ * @return Configured task instance
44
+ */
45
+ internal inline fun <reified T : Task> createTestTask(
46
+ project: Project = createProject(),
47
+ taskName: String = T::class.java.simpleName,
48
+ noinline block: (T) -> Unit = {},
49
+ ): T = project.tasks.register(taskName, T::class.java, block).get()
50
+
51
+ /**
52
+ * Creates a test file with specified content.
53
+ *
54
+ * @param parent Parent directory
55
+ * @param name File name
56
+ * @param content File content
57
+ * @return Created file
58
+ */
59
+ internal fun createTestFile(parent: File, name: String, content: String): File {
60
+ val file = File(parent, name)
61
+ file.parentFile.mkdirs()
62
+ file.writeText(content)
63
+ return file
64
+ }
65
+
66
+ /**
67
+ * Creates a test directory structure.
68
+ *
69
+ * @param parent Parent directory
70
+ * @param path Directory path (can be nested with /)
71
+ * @return Created directory
72
+ */
73
+ internal fun createTestDirectory(parent: File, path: String): File {
74
+ val dir = File(parent, path)
75
+ dir.mkdirs()
76
+ return dir
77
+ }
78
+
79
+ /**
80
+ * Reads a file and returns its content.
81
+ *
82
+ * @param file File to read
83
+ * @return File content as string
84
+ */
85
+ internal fun readFileContent(file: File): String {
86
+ require(file.exists()) { "File does not exist: ${file.absolutePath}" }
87
+ return file.readText()
88
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "project": {
3
+ "name": "test-project",
4
+ "version": "1.0.0",
5
+ "android": {
6
+ "sourceDir": "android",
7
+ "manifestPath": "android/AndroidManifest.xml",
8
+ "packageName": "com.example.testproject"
9
+ }
10
+ },
11
+ "dependencies": {
12
+ "react-native-webview": {
13
+ "name": "react-native-webview",
14
+ "root": "/path/to/node_modules/react-native-webview",
15
+ "platforms": {
16
+ "android": {
17
+ "sourceDir": "android",
18
+ "packageImportPath": "com.reactnativecommunity.webview.RNCWebViewPackage",
19
+ "packageInstance": "new RNCWebViewPackage()",
20
+ "libraryName": "RNCWebView",
21
+ "componentDescriptors": ["RNCWebViewComponentDescriptor"],
22
+ "cmakeListsPath": "android/CMakeLists.txt"
23
+ }
24
+ }
25
+ },
26
+ "react-native-camera": {
27
+ "name": "react-native-camera",
28
+ "root": "/path/to/node_modules/react-native-camera",
29
+ "platforms": {
30
+ "android": {
31
+ "sourceDir": "android",
32
+ "packageImportPath": "org.reactnative.camera.RNCameraPackage",
33
+ "packageInstance": "new RNCameraPackage()",
34
+ "libraryName": "RNCamera",
35
+ "componentDescriptors": null
36
+ }
37
+ }
38
+ },
39
+ "pure-js-module": {
40
+ "name": "pure-js-module",
41
+ "root": "/path/to/node_modules/pure-js-module",
42
+ "platforms": {}
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,16 @@
1
+ //
2
+ // BundleEvaluator.h
3
+ // Pods
4
+ //
5
+ // Created by 오 진성B on 10/8/25.
6
+ //
7
+
8
+ #import <Foundation/Foundation.h>
9
+
10
+ @interface BundleEvaluator : NSObject
11
+
12
+ + (void)evaluate:(NSData *)scriptData
13
+ url:(NSString *)url
14
+ bridgeProxy:(id)bridgeProxy
15
+ completion:(void (^)(NSError *))completion;
16
+ @end
@@ -0,0 +1,76 @@
1
+ //
2
+ // BundleEvaluator.mm
3
+ // Pods
4
+ //
5
+ // Created by 오 진성B on 10/8/25.
6
+ //
7
+
8
+ #import "BundleEvaluator.h"
9
+ #import <React/RCTBridgeProxy.h>
10
+ #import <React-callinvoker/ReactCommon/CallInvoker.h>
11
+ #import <jsi/jsi.h>
12
+
13
+ @interface RCTBridgeProxy (JSIRuntime)
14
+ - (void *)runtime;
15
+ @end
16
+
17
+ @interface RCTBridgeProxy (RCTTurboModule)
18
+ - (std::shared_ptr<facebook::react::CallInvoker>)jsCallInvoker;
19
+ @end
20
+
21
+ @implementation BundleEvaluator
22
+
23
+ + (void)evaluate:(NSData *)scriptData
24
+ url:(NSString *)url
25
+ bridgeProxy:(id)bridgeProxy
26
+ completion:(void (^)(NSError *))completion {
27
+
28
+ // Cast to RCTBridgeProxy
29
+ RCTBridgeProxy *bridgeProxyObj = (RCTBridgeProxy *)bridgeProxy;
30
+
31
+ // Get CallInvoker
32
+ std::shared_ptr<facebook::react::CallInvoker> callInvoker = bridgeProxyObj.jsCallInvoker;
33
+ if (!callInvoker) {
34
+ NSError *error = [NSError errorWithDomain:@"TossBundleLoader"
35
+ code:500
36
+ userInfo:@{NSLocalizedDescriptionKey: @"CallInvoker unavailable"}];
37
+ completion(error);
38
+ return;
39
+ }
40
+
41
+ // Get runtime pointer
42
+ facebook::jsi::Runtime *runtime = (facebook::jsi::Runtime *)bridgeProxyObj.runtime;
43
+ if (!runtime) {
44
+ NSError *error = [NSError errorWithDomain:@"TossBundleLoader"
45
+ code:500
46
+ userInfo:@{NSLocalizedDescriptionKey: @"Runtime unavailable"}];
47
+ completion(error);
48
+ return;
49
+ }
50
+
51
+ // Prepare source
52
+ std::string source{static_cast<const char *>([scriptData bytes]), [scriptData length]};
53
+ std::string sourceUrl{[url UTF8String]};
54
+
55
+ // Execute on JS thread
56
+ callInvoker->invokeAsync([source = std::move(source),
57
+ sourceUrl = std::move(sourceUrl),
58
+ runtime,
59
+ completion]() {
60
+ try {
61
+ runtime->evaluateJavaScript(
62
+ std::make_unique<facebook::jsi::StringBuffer>(std::move(source)),
63
+ sourceUrl
64
+ );
65
+ completion(nil);
66
+ } catch (const std::exception &e) {
67
+ NSString *errorString = [NSString stringWithUTF8String:e.what()];
68
+ NSError *error = [NSError errorWithDomain:@"TossBundleLoader"
69
+ code:500
70
+ userInfo:@{NSLocalizedDescriptionKey: errorString}];
71
+ completion(error);
72
+ }
73
+ });
74
+ }
75
+
76
+ @end
@@ -0,0 +1,91 @@
1
+ //
2
+ // BundleLoader.swift
3
+ // GraniteScreen
4
+ //
5
+ // Protocol for abstracting bundle loading strategies
6
+ //
7
+
8
+ import Foundation
9
+
10
+ // MARK: - BundleLoader Protocol
11
+
12
+ /// Protocol that abstracts bundle loading strategies
13
+ /// Matches Android's BundleLoader interface for cross-platform consistency
14
+ public protocol BundleLoadable {
15
+ /// Loads a bundle and returns its source configuration
16
+ func loadBundle() async throws -> BundleSource
17
+ static func evaluate(scriptData: Data,
18
+ url: String,
19
+ bridgeProxy: Any) async throws
20
+ }
21
+
22
+ // MARK: - BundleSource
23
+
24
+ /// Represents the source of a JavaScript bundle
25
+ /// Matches Android's sealed class BundleSource
26
+ public enum BundleSource {
27
+ /// Development server bundle
28
+ case devServer(host: String = "localhost", port: Int = 8081, moduleName: String, forBundleRoot: String)
29
+
30
+ /// Production bundle from various locations
31
+ case production(location: ProductionLocation, moduleName: String)
32
+
33
+ case customPath(URL, moduleName: String)
34
+ }
35
+
36
+
37
+ /// Represents the location of a production bundle
38
+ public enum ProductionLocation {
39
+ /// Bundle from file system
40
+ case fileSystemBundle(filePath: URL)
41
+
42
+ /// Bundle from app's main bundle
43
+ case embeddedBundle
44
+ }
45
+
46
+ // MARK: - Bundle Loading Error
47
+
48
+ /// Errors that can occur during bundle loading
49
+ public enum BundleLoadError: LocalizedError {
50
+ case networkError(Error)
51
+ case fileNotFound(String)
52
+ case invalidURL(String)
53
+ case downloadFailed(String)
54
+ case metroServerUnavailable
55
+ case bundleNotReady
56
+
57
+ public var errorDescription: String? {
58
+ switch self {
59
+ case .networkError(let error):
60
+ return "Network error: \(error.localizedDescription)"
61
+ case .fileNotFound(let path):
62
+ return "Bundle file not found at: \(path)"
63
+ case .invalidURL(let url):
64
+ return "Invalid bundle URL: \(url)"
65
+ case .downloadFailed(let reason):
66
+ return "Bundle download failed: \(reason)"
67
+ case .metroServerUnavailable:
68
+ return "Metro server is not available"
69
+ case .bundleNotReady:
70
+ return "Bundle is not ready for loading"
71
+ }
72
+ }
73
+ }
74
+
75
+ public extension BundleLoadable {
76
+ public static func evaluate(scriptData: Data,
77
+ url: String,
78
+ bridgeProxy: Any) async throws {
79
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
80
+ BundleEvaluator.evaluate(scriptData,
81
+ url: url,
82
+ bridgeProxy: bridgeProxy) { nsError in
83
+ if let error = nsError {
84
+ continuation.resume(throwing: error)
85
+ } else {
86
+ continuation.resume()
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,7 @@
1
+ // This file is automatically generated by brick-codegen
2
+ // Do not edit manually - regenerate using: brick-codegen
3
+
4
+ import Foundation
5
+
6
+ /// GraniteBundleLoader module types for Brick framework
7
+
@@ -0,0 +1,12 @@
1
+ //
2
+ // GraniteScreen.h
3
+ // GraniteScreen
4
+ //
5
+ // Umbrella header for GraniteScreen framework
6
+ //
7
+
8
+ #import <Foundation/Foundation.h>
9
+
10
+ // Version information
11
+ FOUNDATION_EXPORT double GraniteScreenVersionNumber;
12
+ FOUNDATION_EXPORT const unsigned char GraniteScreenVersionString[];
@@ -0,0 +1,138 @@
1
+ //
2
+ // DefaultViews.swift
3
+ // GraniteScreen
4
+ //
5
+ // Default loading and error views for React Native hosting
6
+ //
7
+
8
+ import UIKit
9
+
10
+ /// Default loading view shown while React Native is loading
11
+ public class DefaultLoadingView: UIView {
12
+ private let activityIndicator = UIActivityIndicatorView(style: .large)
13
+ private let label = UILabel()
14
+
15
+ public override init(frame: CGRect) {
16
+ super.init(frame: frame)
17
+ setupViews()
18
+ }
19
+
20
+ public convenience init() {
21
+ self.init(frame: .zero)
22
+ }
23
+
24
+ required init?(coder: NSCoder) {
25
+ super.init(coder: coder)
26
+ setupViews()
27
+ }
28
+
29
+ private func setupViews() {
30
+ backgroundColor = .systemBackground
31
+
32
+ // Setup activity indicator
33
+ activityIndicator.translatesAutoresizingMaskIntoConstraints = false
34
+ activityIndicator.startAnimating()
35
+ addSubview(activityIndicator)
36
+
37
+ // Setup label
38
+ label.text = "Loading React Native..."
39
+ label.textColor = .label
40
+ label.textAlignment = .center
41
+ label.translatesAutoresizingMaskIntoConstraints = false
42
+ addSubview(label)
43
+
44
+ // Layout constraints
45
+ NSLayoutConstraint.activate([
46
+ activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
47
+ activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20),
48
+
49
+ label.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 16),
50
+ label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
51
+ label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20)
52
+ ])
53
+ }
54
+ }
55
+
56
+ /// Default error view shown when React Native fails to load
57
+ public class DefaultErrorView: UIView {
58
+ private let errorImageView = UIImageView()
59
+ private let titleLabel = UILabel()
60
+ private let messageLabel = UILabel()
61
+ private let retryButton = UIButton(type: .system)
62
+
63
+ private let error: Error
64
+ public var onRetry: (() -> Void)?
65
+
66
+ public init(error: Error) {
67
+ self.error = error
68
+ super.init(frame: .zero)
69
+ setupViews()
70
+ }
71
+
72
+ required init?(coder: NSCoder) {
73
+ fatalError("init(coder:) has not been implemented")
74
+ }
75
+
76
+ private func setupViews() {
77
+ backgroundColor = .systemBackground
78
+
79
+ // Setup error icon
80
+ errorImageView.image = UIImage(systemName: "exclamationmark.triangle.fill")
81
+ errorImageView.tintColor = .systemRed
82
+ errorImageView.contentMode = .scaleAspectFit
83
+ errorImageView.translatesAutoresizingMaskIntoConstraints = false
84
+ addSubview(errorImageView)
85
+
86
+ // Setup title
87
+ titleLabel.text = "Failed to Load"
88
+ titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
89
+ titleLabel.textColor = .label
90
+ titleLabel.textAlignment = .center
91
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
92
+ addSubview(titleLabel)
93
+
94
+ // Setup message
95
+ messageLabel.text = error.localizedDescription
96
+ messageLabel.font = .systemFont(ofSize: 16)
97
+ messageLabel.textColor = .secondaryLabel
98
+ messageLabel.textAlignment = .center
99
+ messageLabel.numberOfLines = 0
100
+ messageLabel.translatesAutoresizingMaskIntoConstraints = false
101
+ addSubview(messageLabel)
102
+
103
+ // Setup retry button
104
+ retryButton.setTitle("Retry", for: .normal)
105
+ retryButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
106
+ retryButton.backgroundColor = .systemBlue
107
+ retryButton.setTitleColor(.white, for: .normal)
108
+ retryButton.layer.cornerRadius = 8
109
+ retryButton.translatesAutoresizingMaskIntoConstraints = false
110
+ retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
111
+ addSubview(retryButton)
112
+
113
+ // Layout constraints
114
+ NSLayoutConstraint.activate([
115
+ errorImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
116
+ errorImageView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -80),
117
+ errorImageView.widthAnchor.constraint(equalToConstant: 60),
118
+ errorImageView.heightAnchor.constraint(equalToConstant: 60),
119
+
120
+ titleLabel.topAnchor.constraint(equalTo: errorImageView.bottomAnchor, constant: 20),
121
+ titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
122
+ titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
123
+
124
+ messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12),
125
+ messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40),
126
+ messageLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40),
127
+
128
+ retryButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 32),
129
+ retryButton.centerXAnchor.constraint(equalTo: centerXAnchor),
130
+ retryButton.widthAnchor.constraint(equalToConstant: 120),
131
+ retryButton.heightAnchor.constraint(equalToConstant: 44)
132
+ ])
133
+ }
134
+
135
+ @objc private func retryTapped() {
136
+ onRetry?()
137
+ }
138
+ }
@@ -0,0 +1,24 @@
1
+ // GraniteDefaultModuleProvider.h
2
+ // GraniteScreen
3
+ //
4
+ // Exposes default module provisioning to Swift
5
+ //
6
+
7
+ #import <Foundation/Foundation.h>
8
+ #if __has_include(<React-RCTAppDelegate/RCTDependencyProvider.h>)
9
+ #import <React-RCTAppDelegate/RCTDependencyProvider.h>
10
+ #else
11
+ #import <React_RCTAppDelegate/RCTDependencyProvider.h>
12
+ #endif
13
+
14
+ NS_ASSUME_NONNULL_BEGIN
15
+
16
+ @interface GraniteDefaultModuleProvider : NSObject
17
+
18
+ + (nullable id)defaultModuleInstanceForClass:(Class)moduleClass
19
+ dependencyProvider:(nullable id<RCTDependencyProvider>)dependencyProvider
20
+ NS_SWIFT_NAME(defaultModuleInstance(for:dependencyProvider:));
21
+
22
+ @end
23
+
24
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,22 @@
1
+ #import "GraniteDefaultModuleProvider.h"
2
+
3
+ // Declared in React-RCTAppDelegate but only exposed in ObjC++ headers.
4
+ #ifdef __cplusplus
5
+ extern "C" id RCTAppSetupDefaultModuleFromClass(
6
+ Class moduleClass,
7
+ id<RCTDependencyProvider> dependencyProvider);
8
+ #else
9
+ extern id RCTAppSetupDefaultModuleFromClass(
10
+ Class moduleClass,
11
+ id<RCTDependencyProvider> dependencyProvider);
12
+ #endif
13
+
14
+ @implementation GraniteDefaultModuleProvider
15
+
16
+ + (id)defaultModuleInstanceForClass:(Class)moduleClass
17
+ dependencyProvider:(id<RCTDependencyProvider>)dependencyProvider
18
+ {
19
+ return RCTAppSetupDefaultModuleFromClass(moduleClass, dependencyProvider);
20
+ }
21
+
22
+ @end
@@ -0,0 +1,103 @@
1
+ //
2
+ // GraniteHostingHelper.swift
3
+ // GraniteScreen
4
+ //
5
+ // Helper extension for setting up React Native hosting with GraniteNativeFactory
6
+ //
7
+
8
+ import React
9
+ import ReactAppDependencyProvider
10
+ import UIKit
11
+
12
+ struct GraniteHostItem {
13
+ var hostURL: URL
14
+ var moduleName: String
15
+ }
16
+
17
+ public enum GraniteReactHostError: Error {
18
+ case bundleLoadError
19
+ case graniteItemConvertError
20
+ case rnHostViewFactoryError
21
+ case bundleURLParsingError
22
+ }
23
+
24
+ private var ReactNativeHostAssociatedKey: UInt8 = 0
25
+ private var ReactNativeDelegateAssociatedKey: UInt8 = 0
26
+ // MARK: - Setup Extension for GraniteReactHost
27
+ /// Extension for classes implementing GraniteReactHost
28
+ /// Provides one-line setup matching Android's pattern
29
+ public extension GraniteReactHost where Self: UIViewController {
30
+
31
+ /// Setup React Native host with automatic lifecycle management
32
+ /// Call this in viewDidLoad() for one-line initialization like Android
33
+ @MainActor
34
+ func setupHost(bundleLoader: BundleLoadable,
35
+ initialProperties: [AnyHashable: Any],
36
+ launchOptions: [AnyHashable: Any]) async {
37
+ graniteSetupDidStart()
38
+ do {
39
+ let bundle = try await bundleLoader.loadBundle()
40
+ do {
41
+ let item = try await convertToGraniteItem(bundleSource: bundle)
42
+ let delegate = ReactNativeFactoryDelegate(url: item.hostURL)
43
+ delegate.dependencyProvider = RCTAppDependencyProvider()
44
+ // Use GraniteNativeFactoryWrapper to enable module customization
45
+ let factory = GraniteNativeFactoryImpl(delegate: delegate)
46
+ factory.reactHost = self // self is GraniteReactHost
47
+ let rnHostView = factory.view(withModuleName: item.moduleName,
48
+ initialProperties: initialProperties,
49
+ launchOptions: launchOptions)
50
+ // 진성님 돌아오고나면 의견나눠보기 objC 기반으로 메모리 등록해서 사용하는방법
51
+ objc_setAssociatedObject(self, &ReactNativeHostAssociatedKey, factory, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
52
+ objc_setAssociatedObject(self, &ReactNativeDelegateAssociatedKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
53
+ if let rnHostView {
54
+ setGraniteBaseView(graniteBaseView: rnHostView)
55
+ graniteSetupDidFinish()
56
+ } else {
57
+ graniteSetupDidError(didFailWith: GraniteReactHostError.rnHostViewFactoryError)
58
+ }
59
+ } catch {
60
+ graniteSetupDidError(didFailWith: GraniteReactHostError.graniteItemConvertError)
61
+ }
62
+ } catch {
63
+ graniteSetupDidError(didFailWith: GraniteReactHostError.bundleLoadError)
64
+ }
65
+ }
66
+
67
+ private func setGraniteBaseView(graniteBaseView: UIView) {
68
+ self.graniteBaseView = graniteBaseView
69
+ view.addSubview(graniteBaseView)
70
+ graniteBaseView.translatesAutoresizingMaskIntoConstraints = false
71
+ NSLayoutConstraint.activate([
72
+ graniteBaseView.topAnchor.constraint(equalTo: view.topAnchor),
73
+ graniteBaseView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
74
+ graniteBaseView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
75
+ graniteBaseView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
76
+ ])
77
+ }
78
+
79
+ private func convertToGraniteItem(bundleSource: BundleSource) async throws -> GraniteHostItem {
80
+ switch bundleSource {
81
+ case .devServer(let host, let port, let moduleName, let forBundleRoot):
82
+ if let url = URL(string: "http://\(host):\(port)/\(forBundleRoot).bundle?platform=ios&dev=true&minify=false") {
83
+ return .init(hostURL: url, moduleName: moduleName)
84
+ } else {
85
+ let url = URL(string: "http://\(host):\(port)/\(forBundleRoot).bundle?platform=ios&dev=true&minify=false")
86
+ return .init(hostURL: url!, moduleName: moduleName)
87
+ }
88
+ case .production(let location, let moduleName):
89
+ switch location {
90
+ case .fileSystemBundle(let filePath):
91
+ return .init(hostURL: filePath, moduleName: moduleName)
92
+ case .embeddedBundle:
93
+ if let url = Bundle.main.url(forResource: "main", withExtension: "jsbundle") {
94
+ return .init(hostURL: url, moduleName: moduleName)
95
+ } else {
96
+ throw GraniteReactHostError.bundleURLParsingError
97
+ }
98
+ }
99
+ case .customPath(let url, let moduleName):
100
+ return .init(hostURL: url, moduleName: moduleName)
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,35 @@
1
+ //
2
+ // GraniteNativeFactory.swift
3
+ // GraniteScreen
4
+ //
5
+ // Swift implementation of GraniteNativeFactory
6
+ //
7
+
8
+ import Foundation
9
+ import ObjectiveC
10
+ import React_RCTAppDelegate
11
+ import UIKit
12
+
13
+ @objcMembers
14
+ open class GraniteNativeFactory: RCTReactNativeFactory {
15
+
16
+ open func view(
17
+ withModuleName moduleName: String,
18
+ initialProperties initProps: [AnyHashable: Any]?,
19
+ launchOptions: [AnyHashable: Any]?
20
+ ) -> UIView? {
21
+ return self.rootViewFactory.view(
22
+ withModuleName: moduleName,
23
+ initialProperties: initProps,
24
+ launchOptions: launchOptions
25
+ )
26
+ }
27
+
28
+ @objc(getModuleInstanceFromClass:)
29
+ open func getModuleInstance(from moduleClass: AnyClass) -> Any? {
30
+ return GraniteDefaultModuleProvider.defaultModuleInstance(
31
+ for: moduleClass,
32
+ dependencyProvider: delegate?.dependencyProvider
33
+ )
34
+ }
35
+ }