Skip to content

Commit c239c31

Browse files
ios: create native-xcode sample
1 parent 3681d9e commit c239c31

File tree

16 files changed

+1068
-0
lines changed

16 files changed

+1068
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ This is a collection of samples showcasing the use of [Node.js on Mobile](https:
44

55
It contains the following samples:
66
* Android: [Native Gradle Sample](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/android/native-gradle)
7+
* iOS: [Native Xcode Sample](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/ios/native-xcode)
78

ios/native-xcode/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# OSX
2+
#
3+
.DS_Store
4+
5+
# Xcode
6+
#
7+
build/
8+
*.pbxuser
9+
!default.pbxuser
10+
*.mode1v3
11+
!default.mode1v3
12+
*.mode2v3
13+
!default.mode2v3
14+
*.perspectivev3
15+
!default.perspectivev3
16+
xcuserdata
17+
*.xccheckout
18+
*.moved-aside
19+
DerivedData
20+
*.hmap
21+
*.ipa
22+
*.xcuserstate
23+
project.xcworkspace
24+
*.framework/

ios/native-xcode/README.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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`.

ios/native-xcode/libnode/copy-libnode.framework-here

Whitespace-only changes.

0 commit comments

Comments
 (0)