|
| 1 | +# Native Xcode Sample |
| 2 | + |
| 3 | +An iOS Xcode project that uses the [`Node.js on Mobile`]( https://github.com/janeasystems/nodejs-mobile) shared library. |
| 4 | + |
| 5 | +The sample app runs the node.js engine in a background thread to start an HTTP server on port 3000 and return the `process.versions` value. The app's Main ViewController UI has a button to query the server and show the server's response. Alternatively, it's also possible to access the server from a browser running on a different device connected to the same local network. |
| 6 | + |
| 7 | +## Prerequisites |
| 8 | +To run the sample on iOS you need: |
| 9 | + - A macOS device with the latest Xcode (Xcode version 9 or greater) with the iOS SDK version 11.0 or higher. |
| 10 | + - One iOS device with arm64 architecture, running iOS version 11.0 or higher. |
| 11 | + - A valid Apple Developer Account. |
| 12 | + |
| 13 | +## How to run |
| 14 | + - Clone this project. |
| 15 | + - Download the Node.js on Mobile shared library from [here](https://github.com/janeasystems/nodejs-mobile/releases/download/nodejs-mobile-v0.1.1/nodejs-mobile-v0.1.1-ios.zip). |
| 16 | + - Copy the `libnode.framework` file inside the zip to this project's `libnode/` folder (there's a `copy-libnode.framework-here` empty file inside the project's folder for convenience). |
| 17 | + - In Xcode import the `ios/native-xcode/native-xcode.xcodeproj` project. |
| 18 | + - Select one physical iOS device as the run target. |
| 19 | + - In the project settings (click on the project main node), in the `Signing` portion of the `General` tab, select a valid Team and handle the provisioning profile creation/update. If you get an error that the bundle identifier cannot be used, you can simply change the bundle identifier to a unique string by appending a few characters to it. |
| 20 | + - Run the app. If the build process doesn't start the app right away, you might have to go to `Settings>General` in the device and enter `Device Management` or `Profiles & Device Management` to manually accept the profile. |
| 21 | + |
| 22 | +## How the sample was developed |
| 23 | + |
| 24 | +### Create an Xcode 9 Project |
| 25 | +Using the Xcode 9's "Create a new Xcode Project" wizard, create a new Project with the following settings, by the order the options appear in screens: |
| 26 | + 1. `ios` `Single View App` template selected |
| 27 | + 1. Entered in the `ProductName` the `native-xcode` name and left the other fields with their defaults, which were: |
| 28 | + - Team: None |
| 29 | + - Organization Name: Janea Systems |
| 30 | + - Organization Identifier: com.janeasystems |
| 31 | + - Language: Objective-C |
| 32 | + - `Use Core Data` unselected |
| 33 | + - `Include Unit Tests` unselected |
| 34 | + - `Include UI Tests` unselected |
| 35 | + 1. Selected a path for my project |
| 36 | + 1. Create |
| 37 | + |
| 38 | +### Add `libnode.framework` to the build process |
| 39 | + |
| 40 | +#### Copy `libnode.framework` to the project structure: |
| 41 | + |
| 42 | +Create the `libnode/` folder path in the project's root folder, next to the `native-xcode.xcodeproj` package. |
| 43 | +Download the [Node.js on Mobile release](https://github.com/janeasystems/nodejs-mobile/releases/download/nodejs-mobile-v0.1.1/nodejs-mobile-v0.1.1-ios.zip), unzip it and copy `libnode.framework` to `libnode/`. |
| 44 | + |
| 45 | +#### Embed the `libnode.framework` in the binary. |
| 46 | + |
| 47 | +In the project settings (click on the project main node), drag the `libnode.framework` file that is inside `libnode/`, from a Finder Window to the `Embedded Binaries` portion of the `General` tab. This will add the framework to both the `Embedded Binaries` and `Linked Frameworks and Libraries` section. |
| 48 | + |
| 49 | +#### Turn `ENABLE_BITCODE` off. |
| 50 | + |
| 51 | +The node binary isn't currently build with bitcode enabled, so, for the time being, we need to disable bitcode for the Application as well. |
| 52 | + |
| 53 | +In the project settings (click on the project main node), in the `Build Options` portion of the `Build Settings` tab, set `Enable Bitcode` to `No`. |
| 54 | + |
| 55 | +### Create the NodeRunner object that will run `nodejs-mobile` |
| 56 | + |
| 57 | +#### Create NodeRunner.h |
| 58 | + |
| 59 | +Create a new `Header File` in the project's structure in the same level as the already existing code files, called `NodeRunner.h`. |
| 60 | + |
| 61 | +This file will contain the following code: |
| 62 | + |
| 63 | +```objectivec |
| 64 | +#ifndef NodeRunner_h |
| 65 | +#define NodeRunner_h |
| 66 | +#import <Foundation/Foundation.h> |
| 67 | + |
| 68 | +@interface NodeRunner : NSObject {} |
| 69 | ++ (void) startEngineWithArguments:(NSArray*)arguments; |
| 70 | +@end |
| 71 | + |
| 72 | +#endif |
| 73 | +``` |
| 74 | + |
| 75 | +#### Create NodeRunner.mm |
| 76 | + |
| 77 | +Create a new `Objective-C File` in the project's structure in the same level as the already existing code files, called `NodeRunner.mm`. The `.mm` extension is important as this will indicate Xcode that this file will contain `C++` code in addition to `Objective-C` code. |
| 78 | + |
| 79 | +This file will contain the following code to start node: |
| 80 | + |
| 81 | +```objectivec++ |
| 82 | +#include "NodeRunner.h" |
| 83 | +#include <libnode/node.hpp> |
| 84 | +#include <string> |
| 85 | +
|
| 86 | +@implementation NodeRunner |
| 87 | +
|
| 88 | +//node's libUV requires all arguments being on contiguous memory. |
| 89 | ++ (void) startEngineWithArguments:(NSArray*)arguments |
| 90 | +{ |
| 91 | + int c_arguments_size=0; |
| 92 | + |
| 93 | + //Compute byte size need for all arguments in contiguous memory. |
| 94 | + for (id argElement in arguments) |
| 95 | + { |
| 96 | + c_arguments_size+=strlen([argElement UTF8String]); |
| 97 | + c_arguments_size++; // for '\0' |
| 98 | + } |
| 99 | + |
| 100 | + //Stores arguments in contiguous memory. |
| 101 | + char* args_buffer=(char*)calloc(c_arguments_size, sizeof(char)); |
| 102 | + |
| 103 | + //argv to pass into node. |
| 104 | + char* argv[[arguments count]]; |
| 105 | + |
| 106 | + //To iterate through the expected start position of each argument in args_buffer. |
| 107 | + char* current_args_position=args_buffer; |
| 108 | + |
| 109 | + //Argc |
| 110 | + int argument_count=0; |
| 111 | + |
| 112 | + //Populate the args_buffer and argv. |
| 113 | + for (id argElement in arguments) |
| 114 | + { |
| 115 | + const char* current_argument=[argElement UTF8String]; |
| 116 | + |
| 117 | + //Copy current argument to its expected position in args_buffer |
| 118 | + strncpy(current_args_position, current_argument, strlen(current_argument)); |
| 119 | + |
| 120 | + //Save current argument start position in argv and increment argc. |
| 121 | + argv[argument_count]=current_args_position; |
| 122 | + argument_count++; |
| 123 | + |
| 124 | + //Increment to the next argument's expected position. |
| 125 | + current_args_position+=strlen(current_args_position)+1; |
| 126 | + } |
| 127 | + |
| 128 | + //Start node, with argc and argv. |
| 129 | + node::Start(argument_count,argv); |
| 130 | +} |
| 131 | +@end |
| 132 | +``` |
| 133 | + |
| 134 | +### Start a background thread to run `startNodeWithArguments`: |
| 135 | + |
| 136 | +The app uses a background thread to run the Node.js engine and it supports to run only one instance of it. |
| 137 | + |
| 138 | +The node code is a simple HTTP server on port 3000 that returns `process.versions`. This is the corresponding node code: |
| 139 | +```js |
| 140 | +var http = require('http'); |
| 141 | +var versions_server = http.createServer( (request, response) => { |
| 142 | + response.end('Versions: ' + JSON.stringify(process.versions)); |
| 143 | +}); |
| 144 | +versions_server.listen(3000); |
| 145 | +``` |
| 146 | + |
| 147 | +For simplicity, the node code is added to the `AppDelegate.m` file. |
| 148 | + |
| 149 | +Add the following line in the file `#import` section: |
| 150 | +```objectivec |
| 151 | +#import "NodeRunner.h" |
| 152 | +``` |
| 153 | + |
| 154 | +Start the thread that runs the node project inside the `didFinishLaunchingWithOptions` selector, which signature should be already have been created by the wizard: |
| 155 | +```objectivec |
| 156 | +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
| 157 | + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ |
| 158 | + NSArray* nodeArguments = [NSArray arrayWithObjects: |
| 159 | + @"node", |
| 160 | + @"-e", |
| 161 | + @"var http = require('http'); " |
| 162 | + " var versions_server = http.createServer( (request, response) => { " |
| 163 | + " response.end('Versions: ' + JSON.stringify(process.versions)); " |
| 164 | + " }); " |
| 165 | + " versions_server.listen(3000); " |
| 166 | + , |
| 167 | + nil |
| 168 | + ]; |
| 169 | + [NodeRunner startEngineWithArguments:nodeArguments]; |
| 170 | + }); |
| 171 | + return YES; |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +### Run the Application |
| 176 | + |
| 177 | +You should now be able to run the application on your physical device. |
| 178 | + |
| 179 | +In the project settings (click on the project main node), in the `Signing` portion of the `General` tab, select a valid Team and handle the provisioning profile creation/update. If you get an error that the bundle identifier cannot be used, you can simply change the bundle identifier to a unique string by appending a few characters to it. |
| 180 | + |
| 181 | +Try to run the app. If the build process doesn't start the app right away, you might have to go to `Settings>General` in the device and enter `Device Management` or `Profiles & Device Management` to manually accept the profile. |
| 182 | + |
| 183 | +### Add simple UI to test |
| 184 | + |
| 185 | +At this point, it's already possible to run the app on an iOS device and access the HTTP server from any device connected to the same local network. If the iOS device's IP is `192.168.1.100` point the browser at `http://192.168.1.100:3000/`. |
| 186 | + |
| 187 | +However, the sample also comes with the UI to query the local HTTP server and show the response. |
| 188 | + |
| 189 | +#### Create Button and TextView |
| 190 | + |
| 191 | +In `Main.storyboard`, use the Xcode interface designer to create a UIButton and a UITextView components. |
| 192 | + |
| 193 | +#### Add UI properties and Connect them |
| 194 | + |
| 195 | +Inside the `ViewController.m` file, add the `IBOutlet` and `IBAction` declarations to the `interface` section: |
| 196 | +```objectivec++ |
| 197 | +@interface ViewController () |
| 198 | +@property (weak, nonatomic) IBOutlet UIButton *myButton; |
| 199 | +@property (weak, nonatomic) IBOutlet UITextView *myTextView; |
| 200 | +
|
| 201 | +- (IBAction)myButtonAction:(id)sender; |
| 202 | +@end |
| 203 | +``` |
| 204 | + |
| 205 | +In the `Assistant Editors` mode of Xcode: |
| 206 | +- Connect the `@property (weak, nonatomic) IBOutlet UITextView *myTextView;` property from `ViewController.m` to the `UITextView` previously created in `Main.storyboard`. |
| 207 | +- Connect the `@property (weak, nonatomic) IBOutlet UIButton *myButton;` property from `ViewController.m` to the `UIButton` previously created in `Main.storyboard`. |
| 208 | +- Connect the `- (IBAction)myButtonAction:(id)sender;` selector from `ViewController.m` to the `UIButton` previously created in `Main.storyboard`. |
| 209 | + |
| 210 | +Add the `- (IBAction)myButtonAction:(id)sender;` definition to the `ViewController.m` `implementation` section: |
| 211 | +```objectivec |
| 212 | +- (IBAction)myButtonAction:(id)sender |
| 213 | +{ |
| 214 | + NSString *localNodeServerURL = @"http:/127.0.0.1:3000/"; |
| 215 | + NSURL *url = [NSURL URLWithString:localNodeServerURL]; |
| 216 | + NSString *versionsData = [NSString stringWithContentsOfURL:url]; |
| 217 | + if (versionsData) |
| 218 | + { |
| 219 | + [_myTextView setText:versionsData]; |
| 220 | + } |
| 221 | + |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +While running the application in your physical device, tapping the UI's `Button` calls the local Node.js's HTTP server and shows the resulting response in the `TextView`. |
0 commit comments