├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── bin └── cli.js ├── lib ├── copyAndReplace.js ├── copyProjectTemplateAndReplace.js ├── makeNewProject.js ├── promptSync.js └── walk.js ├── package.json └── templates ├── App.js └── demo ├── basicview.js ├── cameraview.js ├── index.js ├── ios ├── rn-Bridging-Header.h ├── rnswift_template.xcodeproj │ └── project.pbxproj └── rnswift_template │ ├── rnswift_template.swift │ ├── rnswift_templateBasicViewManager.swift │ ├── rnswift_templateView.swift │ └── rnswift_templateViewManager.swift ├── nativemodule.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | yarn.lock 8 | .DS_Store 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | *.xcuser* 16 | *.xcwork* 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ray Deck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-swift-cli 2 | 3 | Helpers for initializing and getting started with making Swift-based native modules for React Native. 4 | 5 | [![npm version](https://badge.fury.io/js/react-native-swift-cli.svg?style=flat)](https://badge.fury.io/js/react-native-swift-cli) 6 | [![platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg?style=flat)](https://github.com/rhdeck/react-native-swift-cli) 7 | [![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat)](https://github.com/rhdeck/react-native-swift-cli/blob/master/LICENSE) 8 | 9 | # Requirements 10 | 11 | * XCode 9.0 or newer. 12 | * React Native 0.49 or newer (haven't tested it lower than that) 13 | 14 | # Install globally for development 15 | 16 | _react-native-swift-cli_ is a helper utility for initializing new swift-based native modules and UI components. 17 | 18 | ```bash 19 | yarn global add react-native-swift-cli 20 | ``` 21 | 22 | To learn how it works: 23 | 24 | ```bash 25 | react-native-swift --help 26 | ``` 27 | 28 | # How to make a new Swift-based native module 29 | 30 | _react-native-swift_ lets you quickly create a new swift-based native module and get coding. 31 | 32 | ```bash 33 | react-native-swift init myproject 34 | open myproject/ios/*xcode* 35 | code myproject 36 | ``` 37 | 38 | # Usage 39 | 40 | This comes with scripts now! 41 | 42 | ## yarn bridge 43 | 44 | Automatically build the bridge file from Swift to React-Native. No more objective-c coding! Uses [react-native-swift-bridge](https://npmjs.org/package/react-native-swift-bridge) for the building work - check that out for idiosyncracies. 45 | 46 | ## yarn watch 47 | 48 | Will watch your swift files (in the module) for changes, and rebuild your objective-c module on the fly. Super-handy. Best practice is to run as background process so as not to lock up a terminal: 49 | 50 | ```bash 51 | yarn watch & 52 | ``` 53 | 54 | ## yarn addpod 55 | 56 | Utilizes the magic of [react-native-pod](https://npmjs.com/package/react-native-pod) to add Cocoapod dependencies to your module. Here's how: 57 | 58 | ```bash 59 | yarn addpod TwilioVideo --podversion 2.0.0-beta1 60 | ``` 61 | 62 | NOte that this does not install the pod - it just tells your module to list it as a required pod in `package.json`. Then apps that require it will bring the pod to the party through their own `react-native link` process. 63 | 64 | ## yarn removepod 65 | 66 | Undo. `yarn removepod TwilioVideo` 67 | 68 | # Wrapping with a test app 69 | 70 | Don't develop native code from your static library. Never works well. Lots of XCode red lights. Best practice is to work from the context of a runnable app. 71 | 72 | `react-native-swift` makes it easy to create a new almost-blank react-native app and add your swift module. 73 | 74 | ```bash 75 | react-native-swift makeapp myprojecttest myproject 76 | ``` 77 | 78 | **Important**: This installer uses [react-native-setdevteam](https://npmjs.com/package/react-native-setdevteam) and [react-native-bundlebase](https://npmjs.com/package/react-native-bundlebase) to set your development team and start of your bundle ID, respectively. This is going to be important for running the app on the device, which is necessary for demonstrating the demo camera UI component. The impact is that both of these are interactive the first time they run, so will ask you for the bundle base ID (e.g. com.mycompany or org.myname) you want to use, and the 10-digit development team ID to use. (It will search your ~/ directory to extract it from a project you already have if you don't know it off the top of your head) 79 | 80 | # Deployment: adding the module to an existing app 81 | 82 | You can add the Swift-based native module to you app relatively easily. 83 | 84 | ```bash 85 | cd /path/to/myapp 86 | yarn add link:/path/to/myproject # local link 87 | # yarn add @me/myproject # npm registered 88 | # yarn add meongithub/myproject # github fetch 89 | yarn add react-native-swift 90 | react-native link 91 | ``` 92 | 93 | The _react-native-swift_ package will, via react-native link, take care of compatibility between your react native and the Swift based component. 94 | Done! 95 | 96 | Well, almost. **If** you are using a CocoaPod, you will want to do the following: 97 | 98 | ``` 99 | yarn add react-native-pod react-native-fix-pod-links 100 | react-native addpodlinks 101 | react-native link 102 | ``` 103 | 104 | Note that `react-native-fix-pod-links` is only necessary if you are using the locally linked project approach above. 105 | 106 | ## How it works 107 | 108 | Starting in XCode 9.0, you can create static libaries that contain swift code. [Just create swift code the way found on the react-native documentation](https://facebook.github.io/react-native/docs/native-modules-ios.html) and add it to a static library. For reasons unknown, a couple flags need to get set inside the Xcode project file for the app to work with the library. This package forces that issue by adding a blank swift file to the build phases of the app targets, and setting the swift version flag. 109 | 110 | Future versions of Xcode may get less stupid and obviate the need for `react-native-swift`! Hopefully the templates in this package remain helpful. 111 | 112 | Time for me to confess that this is my second FOSS project. Let me know how it works for you! @ me: @ray_deck on twitter and rhdeck on Github. 113 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | let program = require("commander"); 3 | const yarnif = require("yarnif"); 4 | const makeNewProject = require("../lib/makeNewProject"); 5 | const copyAndReplace = require("../lib/copyAndReplace"); 6 | const fs = require("fs"); 7 | const spawnSync = require("child_process").spawnSync; 8 | const chdir = require("process").chdir; 9 | const cwd = require("process").cwd; 10 | const opts = { 11 | encoding: "utf8", 12 | stdio: "inherit" 13 | }; 14 | function validateModuleName(name) { 15 | if (!name.match(/^[$A-Z_][0-9A-Z\-_$]*$/i)) { 16 | console.error( 17 | '"%s" is not a valid name for a module. Please use a valid identifier ' + 18 | "name (alphanumeric with dashes).", 19 | name 20 | ); 21 | process.exit(1); 22 | } 23 | } 24 | function validateAppName(name) { 25 | if (!name.match(/^[$A-Z_][0-9A-Z\_$]*$/i)) { 26 | console.error( 27 | '"%s" is not a valid name for a react-native app. Please use a valid identifier ' + 28 | "name (alphanumeric).", 29 | name 30 | ); 31 | process.exit(1); 32 | } 33 | } 34 | program 35 | .command("init [projectpath]") 36 | .alias("i") 37 | .description("Initialize a new swift-based native module project") 38 | .action(function(projectname, projectpath) { 39 | validateModuleName(projectname); 40 | if (!projectpath) projectpath = "./" + projectname; 41 | makeNewProject(projectname, projectpath); 42 | chdir(projectpath); 43 | yarnif.addDevDependency("react-native-swift-bridge"); 44 | yarnif.addDevDependency("react-native-pod"); 45 | spawnSync("yarn", ["run", "react-native-swift-bridge"], opts); 46 | spawnSync("yarn", ["link"], opts); 47 | }); 48 | 49 | program 50 | .command("makeapp [appprojectpath]") 51 | .alias("m") 52 | .description( 53 | "Create a blank app that adds a swift module to make development easier" 54 | ) 55 | .action(function(appname, swiftpath, appprojectpath) { 56 | validateAppName(appname); 57 | if (!appprojectpath) appprojectpath = "./" + appname; 58 | if (["/", "."].indexOf(swiftpath.substring(0, 1)) == -1) 59 | swiftpath = "./" + swiftpath; 60 | if (swiftpath.substring(0, 1) != "/") swiftpath = cwd() + "/" + swiftpath; 61 | if (!fs.existsSync(swiftpath + "/package.json")) { 62 | console.log("There is no valid project at the path: " + swiftpath + "\n"); 63 | return; 64 | } 65 | const swiftjson = require(swiftpath + "/package.json"); 66 | const swiftprojectname = swiftjson.name; 67 | 68 | spawnSync("react-native", ["init", appname, appprojectpath], opts); 69 | chdir(appprojectpath); 70 | spawnSync("yarn", ["add", "react-native-swift"], opts); 71 | spawnSync("yarn", ["link", swiftprojectname], opts); 72 | spawnSync("yarn", ["add", swiftpath], opts); 73 | spawnSync( 74 | "yarn", 75 | [ 76 | "add", 77 | "react-native-fix-pod-links", 78 | "react-native-xcode", 79 | "react-native-setdevteam", 80 | "react-native-bundlebase", 81 | "react-native-fix-ios-version", 82 | "react-native-camera-ios-enable" 83 | ], 84 | opts 85 | ); 86 | spawnSync("react-native", ["addpodlinks"], opts); 87 | spawnSync("react-native", ["setdevteam"], opts); 88 | spawnSync("react-native", ["link"], opts); 89 | copyAndReplace(__dirname + "/../templates/App.js", "./App.js", { 90 | rnswifttemplate: swiftprojectname 91 | }); 92 | console.log( 93 | 'Done. To edit your project in xcode, type "react-native xcode"\nNote that if you want to deploy a cocoapod in this app (e.g. after running addpod on your module), you should "yarn add react-native-pod"' 94 | ); 95 | }); 96 | program 97 | .command("*") 98 | .description("All malformed commands display this help") 99 | .action(function() { 100 | program.outputHelp(); 101 | }); 102 | program.parse(process.argv); 103 | 104 | if (!process.argv.slice(2).length) { 105 | program.outputHelp(); 106 | } 107 | -------------------------------------------------------------------------------- /lib/copyAndReplace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | 14 | // Binary files, don't process these (avoid decoding as utf8) 15 | const binaryExtensions = ['.png', '.jar']; 16 | 17 | /** 18 | * Copy a file to given destination, replacing parts of its contents. 19 | * @param srcPath Path to a file to be copied. 20 | * @param destPath Destination path. 21 | * @param replacements: e.g. {'TextToBeReplaced': 'Replacement'} 22 | * @param contentChangedCallback 23 | * Used when upgrading projects. Based on if file contents would change 24 | * when being replaced, allows the caller to specify whether the file 25 | * should be replaced or not. 26 | * If null, files will be overwritten. 27 | * Function(path, 'identical' | 'changed' | 'new') => 'keep' | 'overwrite' 28 | */ 29 | function copyAndReplace(srcPath, destPath, replacements, contentChangedCallback) { 30 | if (fs.lstatSync(srcPath).isDirectory()) { 31 | if (!fs.existsSync(destPath)) { 32 | fs.mkdirSync(destPath); 33 | } 34 | // Not recursive 35 | return; 36 | } 37 | 38 | const extension = path.extname(srcPath); 39 | if (binaryExtensions.indexOf(extension) !== -1) { 40 | // Binary file 41 | let shouldOverwrite = 'overwrite'; 42 | if (contentChangedCallback) { 43 | const newContentBuffer = fs.readFileSync(srcPath); 44 | let contentChanged = 'identical'; 45 | try { 46 | const origContentBuffer = fs.readFileSync(destPath); 47 | if (Buffer.compare(origContentBuffer, newContentBuffer) !== 0) { 48 | contentChanged = 'changed'; 49 | } 50 | } catch (err) { 51 | if (err.code === 'ENOENT') { 52 | contentChanged = 'new'; 53 | } else { 54 | throw err; 55 | } 56 | } 57 | shouldOverwrite = contentChangedCallback(destPath, contentChanged); 58 | } 59 | if (shouldOverwrite === 'overwrite') { 60 | copyBinaryFile(srcPath, destPath, (err) => { 61 | if (err) { throw err; } 62 | }); 63 | } 64 | } else { 65 | // Text file 66 | const srcPermissions = fs.statSync(srcPath).mode; 67 | let content = fs.readFileSync(srcPath, 'utf8'); 68 | Object.keys(replacements).forEach(regex => 69 | content = content.replace(new RegExp(regex, 'g'), replacements[regex]) 70 | ); 71 | 72 | let shouldOverwrite = 'overwrite'; 73 | if (contentChangedCallback) { 74 | // Check if contents changed and ask to overwrite 75 | let contentChanged = 'identical'; 76 | try { 77 | const origContent = fs.readFileSync(destPath, 'utf8'); 78 | if (content !== origContent) { 79 | //console.log('Content changed: ' + destPath); 80 | contentChanged = 'changed'; 81 | } 82 | } catch (err) { 83 | if (err.code === 'ENOENT') { 84 | contentChanged = 'new'; 85 | } else { 86 | throw err; 87 | } 88 | } 89 | shouldOverwrite = contentChangedCallback(destPath, contentChanged); 90 | } 91 | if (shouldOverwrite === 'overwrite') { 92 | fs.writeFileSync(destPath, content, { 93 | encoding: 'utf8', 94 | mode: srcPermissions, 95 | }); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Same as 'cp' on Unix. Don't do any replacements. 102 | */ 103 | function copyBinaryFile(srcPath, destPath, cb) { 104 | let cbCalled = false; 105 | const srcPermissions = fs.statSync(srcPath).mode; 106 | const readStream = fs.createReadStream(srcPath); 107 | readStream.on('error', function(err) { 108 | done(err); 109 | }); 110 | const writeStream = fs.createWriteStream(destPath, { 111 | mode: srcPermissions 112 | }); 113 | writeStream.on('error', function(err) { 114 | done(err); 115 | }); 116 | writeStream.on('close', function(ex) { 117 | done(); 118 | }); 119 | readStream.pipe(writeStream); 120 | function done(err) { 121 | if (!cbCalled) { 122 | cb(err); 123 | cbCalled = true; 124 | } 125 | } 126 | } 127 | 128 | module.exports = copyAndReplace; 129 | -------------------------------------------------------------------------------- /lib/copyProjectTemplateAndReplace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | "use strict"; 10 | 11 | const chalk = require("chalk"); 12 | const copyAndReplace = require("./copyAndReplace"); 13 | const path = require("path"); 14 | const prompt = require("./promptSync")(); 15 | const walk = require("./walk"); 16 | 17 | /** 18 | * Util for creating a new React Native project. 19 | * Copy the project from a template and use the correct project name in 20 | * all files. 21 | * @param srcPath e.g. '/Users/martin/AwesomeApp/node_modules/react-native/local-cli/templates/HelloWorld' 22 | * @param destPath e.g. '/Users/martin/AwesomeApp' 23 | * @param newProjectName e.g. 'AwesomeApp' 24 | * @param options e.g. { 25 | * upgrade: true, 26 | * force: false, 27 | * displayName: 'Hello World', 28 | * ignorePaths: ['template/file/to/ignore.md'], 29 | * } 30 | */ 31 | function copyProjectTemplateAndReplace( 32 | srcPath, 33 | destPath, 34 | newProjectName, 35 | options 36 | ) { 37 | if (!srcPath) { 38 | throw new Error("Need a path to copy from"); 39 | } 40 | if (!destPath) { 41 | throw new Error("Need a path to copy to"); 42 | } 43 | if (!newProjectName) { 44 | throw new Error("Need a project name"); 45 | } 46 | 47 | options = options || {}; 48 | 49 | walk(srcPath).forEach(absoluteSrcFilePath => { 50 | // 'react-native upgrade' 51 | if (options.upgrade) { 52 | // Don't upgrade these files 53 | const fileName = path.basename(absoluteSrcFilePath); 54 | // This also includes __tests__/index.*.js 55 | if (fileName === "index.ios.js") { 56 | return; 57 | } 58 | if (fileName === "index.android.js") { 59 | return; 60 | } 61 | if (fileName === "index.js") { 62 | return; 63 | } 64 | if (fileName === "App.js") { 65 | return; 66 | } 67 | } 68 | 69 | const relativeFilePath = path.relative(srcPath, absoluteSrcFilePath); 70 | const safeProjectName = newProjectName.replace(/-/g, "_"); 71 | const relativeRenamedPath = dotFilePath(relativeFilePath) 72 | .replace(/rnswift_template/g, safeProjectName) 73 | .replace(/rnswifttemplate/g, newProjectName); 74 | //.replace(/helloworld/g, newProjectName.toLowerCase()); 75 | 76 | // Templates may contain files that we don't want to copy. 77 | // Examples: 78 | // - Dummy package.json file included in the template only for publishing to npm 79 | // - Docs specific to the template (.md files) 80 | if (options.ignorePaths) { 81 | if (!Array.isArray(options.ignorePaths)) { 82 | throw new Error("options.ignorePaths must be an array"); 83 | } 84 | if ( 85 | options.ignorePaths.some(ignorePath => ignorePath === relativeFilePath) 86 | ) { 87 | // Skip copying this file 88 | return; 89 | } 90 | } 91 | 92 | let contentChangedCallback = null; 93 | if (options.upgrade && !options.force) { 94 | contentChangedCallback = (_, contentChanged) => { 95 | return upgradeFileContentChangedCallback( 96 | absoluteSrcFilePath, 97 | relativeRenamedPath, 98 | contentChanged 99 | ); 100 | }; 101 | } 102 | copyAndReplace( 103 | absoluteSrcFilePath, 104 | path.resolve(destPath, relativeRenamedPath), 105 | { 106 | "RNSwift DisplayName": options.displayName || newProjectName, 107 | rnswifttemplate: newProjectName, 108 | rnswift_template: safeProjectName 109 | //'helloworld': newProjectName.toLowerCase(), 110 | }, 111 | contentChangedCallback 112 | ); 113 | }); 114 | } 115 | 116 | /** 117 | * There are various dotfiles in the templates folder in the RN repo. We want 118 | * these to be ignored by tools when working with React Native itself. 119 | * Example: _babelrc file is ignored by Babel, renamed to .babelrc inside 120 | * a real app folder. 121 | * This is especially important for .gitignore because npm has some special 122 | * behavior of automatically renaming .gitignore to .npmignore. 123 | */ 124 | function dotFilePath(path) { 125 | if (!path) return path; 126 | return path 127 | .replace("_gitignore", ".gitignore") 128 | .replace("_gitattributes", ".gitattributes") 129 | .replace("_babelrc", ".babelrc") 130 | .replace("_flowconfig", ".flowconfig") 131 | .replace("_buckconfig", ".buckconfig") 132 | .replace("_watchmanconfig", ".watchmanconfig"); 133 | } 134 | 135 | function upgradeFileContentChangedCallback( 136 | absoluteSrcFilePath, 137 | relativeDestPath, 138 | contentChanged 139 | ) { 140 | if (contentChanged === "new") { 141 | console.log(chalk.bold("new") + " " + relativeDestPath); 142 | return "overwrite"; 143 | } else if (contentChanged === "changed") { 144 | console.log( 145 | chalk.bold(relativeDestPath) + 146 | " " + 147 | "has changed in the new version.\nDo you want to keep your " + 148 | relativeDestPath + 149 | " or replace it with the " + 150 | "latest version?\nIf you ever made any changes " + 151 | "to this file, you'll probably want to keep it.\n" + 152 | "You can see the new version here: " + 153 | absoluteSrcFilePath + 154 | "\n" + 155 | "Do you want to replace " + 156 | relativeDestPath + 157 | "? " + 158 | "Answer y to replace, n to keep your version: " 159 | ); 160 | const answer = prompt(); 161 | if (answer === "y") { 162 | console.log("Replacing " + relativeDestPath); 163 | return "overwrite"; 164 | } else { 165 | console.log("Keeping your " + relativeDestPath); 166 | return "keep"; 167 | } 168 | } else if (contentChanged === "identical") { 169 | return "keep"; 170 | } else { 171 | throw new Error( 172 | `Unkown file changed state: ${relativeDestPath}, ${contentChanged}` 173 | ); 174 | } 175 | } 176 | 177 | module.exports = copyProjectTemplateAndReplace; 178 | -------------------------------------------------------------------------------- /lib/makeNewProject.js: -------------------------------------------------------------------------------- 1 | const copyProjectTemplateAndReplace = require("./copyProjectTemplateAndReplace") 2 | function makeNewProject(projectname, projectpath, sourcePath = null) { 3 | console.log("Creating project " + projectname + " at path " + projectpath); 4 | //Open path to templates 5 | //Walk tree - copying as I go 6 | //look for files with the "rnswifttemplate" name, and replace with the projectname 7 | if(!sourcePath) sourcePath = __dirname + "/../templates/demo/" 8 | console.log("Copying from sourcePath: " + sourcePath); 9 | copyProjectTemplateAndReplace(sourcePath, projectpath, projectname); 10 | console.log("Done. Created project " + projectname + " at " + projectpath); 11 | } 12 | module.exports = makeNewProject 13 | -------------------------------------------------------------------------------- /lib/promptSync.js: -------------------------------------------------------------------------------- 1 | // Simplified version of: 2 | // https://github.com/0x00A/prompt-sync/blob/master/index.js 3 | 4 | 'use strict'; 5 | 6 | var fs = require('fs'); 7 | var term = 13; // carriage return 8 | 9 | function create() { 10 | 11 | return prompt; 12 | 13 | function prompt(ask, value, opts) { 14 | var insert = 0, savedinsert = 0, res, i, savedstr; 15 | opts = opts || {}; 16 | 17 | if (Object(ask) === ask) { 18 | opts = ask; 19 | ask = opts.ask; 20 | } else if (Object(value) === value) { 21 | opts = value; 22 | value = opts.value; 23 | } 24 | ask = ask || ''; 25 | var echo = opts.echo; 26 | var masked = 'echo' in opts; 27 | 28 | var fd = (process.platform === 'win32') ? 29 | process.stdin.fd : 30 | fs.openSync('/dev/tty', 'rs'); 31 | 32 | var wasRaw = process.stdin.isRaw; 33 | if (!wasRaw) { process.stdin.setRawMode(true); } 34 | 35 | var buf = new Buffer(3); 36 | var str = '', character, read; 37 | 38 | savedstr = ''; 39 | 40 | if (ask) { 41 | process.stdout.write(ask); 42 | } 43 | 44 | var cycle = 0; 45 | var prevComplete; 46 | 47 | while (true) { 48 | read = fs.readSync(fd, buf, 0, 3); 49 | if (read > 1) { // received a control sequence 50 | if (buf.toString()) { 51 | str = str + buf.toString(); 52 | str = str.replace(/\0/g, ''); 53 | insert = str.length; 54 | process.stdout.write('\u001b[2K\u001b[0G'+ ask + str); 55 | process.stdout.write('\u001b[' + (insert+ask.length+1) + 'G'); 56 | buf = new Buffer(3); 57 | } 58 | continue; // any other 3 character sequence is ignored 59 | } 60 | 61 | // if it is not a control character seq, assume only one character is read 62 | character = buf[read-1]; 63 | 64 | // catch a ^C and return null 65 | if (character == 3){ 66 | process.stdout.write('^C\n'); 67 | fs.closeSync(fd); 68 | process.exit(130); 69 | process.stdin.setRawMode(wasRaw); 70 | return null; 71 | } 72 | 73 | // catch the terminating character 74 | if (character == term) { 75 | fs.closeSync(fd); 76 | break; 77 | } 78 | 79 | if (character == 127 || (process.platform == 'win32' && character == 8)) { //backspace 80 | if (!insert) continue; 81 | str = str.slice(0, insert-1) + str.slice(insert); 82 | insert--; 83 | process.stdout.write('\u001b[2D'); 84 | } else { 85 | if ((character < 32 ) || (character > 126)) 86 | continue; 87 | str = str.slice(0, insert) + String.fromCharCode(character) + str.slice(insert); 88 | insert++; 89 | }; 90 | 91 | if (masked) { 92 | process.stdout.write('\u001b[2K\u001b[0G' + ask + Array(str.length+1).join(echo)); 93 | } else { 94 | process.stdout.write('\u001b[s'); 95 | if (insert == str.length) { 96 | process.stdout.write('\u001b[2K\u001b[0G'+ ask + str); 97 | } else { 98 | if (ask) { 99 | process.stdout.write('\u001b[2K\u001b[0G'+ ask + str); 100 | } else { 101 | process.stdout.write('\u001b[2K\u001b[0G'+ str + '\u001b[' + (str.length - insert) + 'D'); 102 | } 103 | } 104 | process.stdout.write('\u001b[u'); 105 | process.stdout.write('\u001b[1C'); 106 | } 107 | 108 | } 109 | 110 | process.stdout.write('\n') 111 | 112 | process.stdin.setRawMode(wasRaw); 113 | 114 | return str || value || ''; 115 | }; 116 | }; 117 | 118 | module.exports = create; -------------------------------------------------------------------------------- /lib/walk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | 14 | function walk(current) { 15 | if (!fs.lstatSync(current).isDirectory()) { 16 | return [current]; 17 | } 18 | 19 | const files = fs.readdirSync(current).map(child => { 20 | child = path.join(current, child); 21 | return walk(child); 22 | }); 23 | return [].concat.apply([current], files); 24 | } 25 | 26 | module.exports = walk; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-swift-cli", 3 | "version": "2.3.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "chalk": "^2.3.0", 7 | "commander": "^2.11.0", 8 | "glob": "^7.1.2", 9 | "react-native-swift": "^1.0.0", 10 | "react-native-swift-bridge": "rhdeck/react-native-swift-bridge", 11 | "xcode": "rhdeck/cordova-node-xcode", 12 | "yarnif": "^1.4.0" 13 | }, 14 | "author": { 15 | "name": "Ray Deck", 16 | "email": "ray@raydeck.com", 17 | "url": "https://github.com/rhdeck" 18 | }, 19 | "bin": { 20 | "react-native-swift": "./bin/cli.js", 21 | "rns": "./bin/cli.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/rhdeck/react-native-swift-cli.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | Platform, 4 | StyleSheet, 5 | Text, 6 | View, 7 | TouchableOpacity, 8 | Button, 9 | Image, 10 | ScrollView, 11 | SafeAreaView 12 | } from "react-native"; 13 | import { NativeMod, BasicView, CameraView } from "rnswifttemplate"; 14 | 15 | export default class App extends Component { 16 | state = { 17 | instructions: "Template Instructions", 18 | counter: 0, 19 | cameraFront: true, 20 | imageURL: "https://facebook.github.io/react-native/docs/assets/favicon.png" 21 | }; 22 | async componentWillMount() { 23 | var me = this; 24 | NativeMod.addListener(arr => { 25 | this.setState(previousState => { 26 | const newcounter = previousState.counter + 1; 27 | const newmessage = "Again a number: " + newcounter.toString(); 28 | setTimeout(() => { 29 | NativeMod.emitMessage(newmessage, 0); 30 | }, 500); 31 | return { 32 | instructions: "Received message: " + arr.message, 33 | counter: newcounter 34 | }; 35 | }); 36 | }); 37 | await NativeMod.emitMessage("Starting"); 38 | try { 39 | const reply = await NativeMod.demoWithPromise("Hello there"); 40 | console.log("Got a promise back", reply); 41 | this.setState({ promiseMessage: reply }); 42 | } catch (e) { 43 | console.log("Got an error back"); 44 | this.setState({ promiseMessage: "failed promise!" }); 45 | } 46 | } 47 | render() { 48 | const constMessage = "My constant startTime is " + NativeMod.startTime; 49 | return ( 50 | 51 | 52 | 59 | Welcome to React Native Swift! 60 | {constMessage} 61 | 62 | Edit App.js and your plugin index.js for live edits. 63 | 64 | {this.state.instructions} 65 | 66 | Promise: {this.state.promiseMessage} 67 | 68 | 69 | 78 | 79 | 80 | Starting with a basic native view. That's the green thing. 81 | Pretty boring. 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | This is a native camera view. You can control direction via 90 | props (set through the button below). Tap the image to take a 91 | photo. 92 | 93 | 94 |