App for iphone/android displaying guides on specific locations in helsingborg. Integrated with event-api.
- Getting started
- Monorepo
- Working with cities
- Adding a new city
- API integration
- Local development
- Overall solution
- Native components
- Build for release
- Clone repo from Repo
- Change to the directory of your chosen city in
./cities
- Run
yarn install
to install node packages - Run
yarn start
to get Metro running - Run
npx react-native run-ios --device
ornpx react-native run-android
to build the app as normal
We used this article to setup the monorepo and code sharing.
Setting this up included changing a lot of references to node_modules
in the React Native projects (iOS/Android).
For jetifier
there's a postinstall
script in each cities packages.json
to run it in the root directory. There's an open issue that would hopefully remove that need.
The javascript code is shared between cities, so any changes are made in the ./guide-app/src
directory.
To facilitate that, we use yarn workspaces, meaning all npm packages lives in ./node_modules
. The configuration for that is in ./package.json
"workspaces": [
"cities/*",
"guide-app"
],
For each app to be able to build, we use a babel.config.js
with module-resolver
aliases that point to ./guide-app
. The only exceptions are @assets
(points to guide-app/assets/[city]
) and @data
(e.g cities/Lund/data
).
There is also a metro.config.js
in each city folder that makes sure Metro looks for packages in ./node_modules
.
NOTE This hasn't been tested after moving to monorepo structure to use with care.
- In the top level of this repository run the add_city shell script with the name of the city and the ios and android bundle ids
e.g.
sh add_city.sh Helsingborg org.hbg.GuideHbg com.guidehbg
- Also in the top level run the add_city_assets shell script to create new override asset directories under assets
e.g.
sh add_city_assets.sh Helsingborg
- Replace the templated assets in the assets/city_name folder with city-specific ones
- Run the update_city shell script in the top level of this repository to copy the assets from this override directory into the actual city project
e.g.
sh update_city.sh Helsingborg
- The metro bundler and any watchman watches should be killed as they get into a confused state
To debug the Javascript packager will have to run in background. This will give the option for hotreloding and remote debugging Javascript from Chrome. When building for release this features is disabled.
NOTE This hasn't been tested after moving to monorepo structure to use with care.
Once you have a city-specific assets directory (see below) make any asset changes in the ./assets/city_name
directory.
Once the changes are ready, run the update_city shell script to copy all of the changed files into the city project (in ./cities/city_name)
Each of these are separate ReactNative projects with the aim of building completely separate guide apps.
To fetch data from the API for respective city, the app uses a GROUP_ID
, found in [city]/data/endpoints.js
.
Prerequisites:
pod install
If you want to build from command line:
- open root folder from terminal or cmd.
- Type cmd
react-native run-ios
Simulator will start as well as the javascript packager
If you want to debug from Xcode
- Open Xcode
- Choose
GuideHbg schema
and device you want to debug on(simulated or on device) in dropdown in the top of the window. - Press play
- Make sure your device is on the same network if you run on physical device.
Prerequisites:
- Create a file
android/release.properties
that contains
storeFile=../relative/path/to/release.keystore
If you want to build from command line:
- Go to root folder of solution
- Type
react-native run-android
If you want to debug from Android studio
- Open android studio
- Click the bug symbol with the little play button on top in the ribbon.
Most of the user interface(UI) is created in react native with JSX syntax. Some of the UI elements is also based on the native components which comes with react.
The basic architechture follow standard react flow with state and props. The data flow is handled with redux which is handling data in a single store across the entire application. Data only flow in one direction having the store as single atomic state.
The basic folder structure for the react part is as follows:
root
│ index.js
└───src
│ │
| └───components
| │ └─── screens
| │ └─── shared
| │
| └───service
└───android
└───ios
|
The views are gathered in the screens folder in which act as the house for different components ans modules. The shared folder holds components that can be used by different views.
The services folder has most of the view logic of the application as well as communication between custom native components(ios and android) and react native.
The application has some custom native compnentes both in android and in IOS. Mostly to communicate with beacons trough bluetooth. The native componentes share the same contract(as much as possible) so that the implemenation can be fairly the the same for IOS and android.
The beacon technology is based on bluetooth low energy(BLE). There is essentially two protocols used for this, Eddystone and iBeacon. iBeacon is a simple api for IOS and has no equvalent in android therefore Eddystone was choosen.
The eddystone protocol implementation for IOS is forked from EssBeaconScanner. The library has a simple library for start scanning reading some information from the beacon and determin the type.
The IosBeacon.m uses some of the functions in the beacon library and exposes them to react.
The most important methods are:
- (void)beaconScanner:(ESSBeaconScanner *)scanner
didFindBeacon:(id)beaconInfo {
ESSBeaconInfo *_beaconInfo = (ESSBeaconInfo *) beaconInfo;
[self sendEventWithName:@"BEACON_ENTERED_REGION_IOS" body:[NSString stringWithFormat:@"{\"txPower\":\"%@\",\"rssi\":\"%@\",\"uid\":\"%@\"}",_beaconInfo.txPower,_beaconInfo.RSSI, _beaconInfo.beaconID.beaconID]];
NSLog(@"I Saw an Eddystone!: %@",beaconInfo );
}
-(void)beaconScanner:(ESSBeaconScanner *)scanner didUpdateBeacon:(id)beaconInfo{
ESSBeaconInfo *_beaconInfo = (ESSBeaconInfo *) beaconInfo;
[self sendEventWithName:@"UPDATE_BEACON_DATA" body:[NSString stringWithFormat:@"{\"txPower\":\"%@\",\"rssi\":\"%@\",\"uid\":\"%@\"}",_beaconInfo.txPower,_beaconInfo.RSSI, _beaconInfo.beaconID.beaconID]];
NSLog(@"I Saw an Eddystone!: %@",beaconInfo );
}
The method will push all the beacons that is in proximity to the React context.
Notification are also abstracted in a native method NotificationManager.m
Method exposed to React:
RCT_EXPORT_METHOD(showMediaNotification:(NSString *)title:(NSString *)body:(NSString *)identifier)
{
[self requestNotification:title body:body identifier:identifier];
}
This will simply display a notification from the application.
The AudioManager exposes many methods to React for handling playback of audio.
- First the init method takes a file that can be played
RCT_EXPORT_METHOD(init:(NSString *)fileName title:(NSString*) title body:(NSString*) body )
Control methods are:
- start
RCT_EXPORT_METHOD(start)
- stop
RCT_EXPORT_METHOD(stop)
- pause
RCT_EXPORT_METHOD(pause)
- seekTo
RCT_EXPORT_METHOD(seekTo:(NSInteger)seconds)
Also some meta data methods is exposed *isPlaying
RCT_REMAP_METHOD(isPlaying,
resolveIsPlaying:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
*getCurrentPosition
```objc
RCT_REMAP_METHOD(getCurrentPosition,
resolverCurrentTime:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
*getDuration
RCT_REMAP_METHOD(getDuration,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
The BeaconModule int android is a bit more complex then in IOS or at least have one more step in order to start scanning.
First sted is to init:
public void init(Promise promise)
Then in react listen on BEACON_SERVICE_CONNECTED
(more on the messaging system between react and android here)
After the the application can start scanning for beacons.
public void startRangingBeacons(String regionId, Promise promise)
Stop scanning is also exposed so the application can stop scanning when navigating around the application.
public void stopRangingBeacons(String regionId, Promise promise)
The MediaModule.java in android exposes the same methods as the IOS AudioModule. Which makes the implementation in React simple.
Here is the exposed methods:
@ReactMethod
public void init(String url){
try {
this.release();
}catch (Exception e){
e.printStackTrace();
}
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
mediaPlayer.setDataSource(url);
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnErrorListener(this);
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
@ReactMethod
public void start(){
if(mediaPlayer !=null)
mediaPlayer.start();
}
@ReactMethod
public void pause(){
if(mediaPlayer !=null)
mediaPlayer.pause();
}
@ReactMethod
public void stop(){
if(mediaPlayer !=null)
mediaPlayer.stop();
}
@ReactMethod
public void release(){
if(mediaPlayer !=null) {
mediaPlayer.release();
mediaPlayer = null;
}
}
@ReactMethod
public void getCurrentPosition(Promise jsPromise){
if(mediaPlayer !=null){
int position = mediaPlayer.getCurrentPosition();
jsPromise.resolve(position);
}
}
@ReactMethod
public void isPlaying(Promise jsPromise){
if(mediaPlayer !=null){
boolean isPlaying = mediaPlayer.isPlaying();
jsPromise.resolve(isPlaying);
}
}
@ReactMethod
public void getDuration(Promise jsPromise){
if(mediaPlayer !=null){
int duration = mediaPlayer.getDuration();
jsPromise.resolve(duration);
}
}
@ReactMethod
public void seekTo(int newPosition){
if(mediaPlayer !=null){
mediaPlayer.seekTo(newPosition);
}
}
To build for App Store follow the steps on this page.
The steps to upload to App Store are:
In XCode:
- Set release build configuration
- Product > Scheme > Edit scheme
-
Bump build number
-
Create archive
- Choose generic device from device menu.
- Select menu Product->Archive
- Upload package
- Select Window->Organizer
- Press upload to app store(This will only upload to itunes connect)
- Follow the steps choosing the correct account.
Building for release in Android is pretty much covered here
make sure to create a file android/release.properties
that contains
storeFile=../relative/path/to/release.keystore
keyAlias=key-alias
keyPassword=key-password
storePassword=store-password
Then running cd android && ./gradlew assembleRelease
from root folder in application.