- 31 Jan 2024
- 7 Minutes to read
- Print
- DarkLight
React Native - iOS custom workout
- Updated on 31 Jan 2024
- 7 Minutes to read
- Print
- DarkLight
Sency Motion React Native Custom Workout Flow
In this article I am going to show you how to add a custom workout flow to your app
1. The first thing that we would like to do is to install the SDK to your iOS app
To do so, in the iOS folder open and edit the Podfile. Add the following code:
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
source 'https://bitbucket.org/sency-ios/sency_ios_sdk.git' # 1
source 'https://github.com/CocoaPods/Specs.git' # 1
platform :ios, '14.0'
install! 'cocoapods', :deterministic_uuids => false
target 'MotionReactDemo' do
use_frameworks! # 2
pod 'SencyMotionLib' # 3
config = use_native_modules!
# Flags change depending on the env values.
flags = get_default_flags()
use_react_native!(
:path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => flags[:hermes_enabled],
:fabric_enabled => flags[:fabric_enabled],
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
target 'MotionReactDemoTests' do
inherit! :complete
# Pods for testing
end
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
# use_flipper!() # 4
post_install do |installer|
react_native_post_install(installer)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' # 5
end
end
end
end
Marked in # is the code you need to add:
Let Cocoapods know about the source URL's
Tells Cocoapods to use dynamic libraries
Add SencyMotionLib pod
Comment these lines.
Post-install hooks that allow you to run the SDK in a simulator.
Now in the terminal go to the iOS folder and run the following command:
pod install --repo-update
And that's it - the SDK is installed and we can start writing code.
2. Now let's add some code to the iOS App:
First, open the .xcworkspace file and Xcode will open (you can use a shell command from the root directory).
xed .
we will need to create 5 files (SDKView.swift, SDKViewController.swift, SDKViewManager.swift, SDKViewManager.m, and Bridging-Header)
2.1 Configuring SencyMotion
In order to configure we will need to create an Objective-C class let's call it SDKConfigure
Right-click the project folder and select New File...
Select "Cocoa Touch Class" and then add the name SDKConfigure and make sure that the language is Objective-C and a subclass of NSObject
After that, you should have 2 new files SDKConfigure.h and SDKConfigure.m
In SDKConfigure.h add the following:
#import <Foundation/Foundation.h>@interface SDKConfigure : NSObject+(void)configure;@end
In this code, we add a static function to SDKConfigure.h
now In SDKConfigure.m we need to add configure func implementation
#import "SDKInitalizer.h"#import <SencyMotionLib/SencyMotionLib-Swift.h>@implementation SDKConfigure +(void)configure{ [SencyMotion configureWithAuthKey:@"YOUR_KEY" onFailure:^(NSError *error) { NSLog(@"Error: %@", error.localizedDescription); }];}@end
In this code, we add the SencyMotionLib framework to the file and called the configure function with the following:
AuthKey: the key that you got from Sency
onFailure: a callback in case of an error
Now we are ready to use SDKConfigure class
open AppDelegate.mm and add this line to the top
#import "SDKConfigure.h"
Then in this function
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
Add this line to the top
[SDKConfigure configure];
2.2 let's create the SDKView and the Bridging-Header
Right-click the project folder and select New File...
Select 'Swift File' and enter file name SDKView.swift and press 'Create'
A popup will appear that will ask you if you want a Bridging-Header (that's right Xcode will create a Bridging-Header for us) press yes and the two files will be created.
Now please go to the Bridging-Header (should be called yourAppName-Bridging-Header.h) and add the following code:
#import "React/RCTBridgeModule.h"#import "React/RCTViewManager.h"#import "React/RCTEventEmitter.h"
Now we can start adding the code for our UI.
2.3 Adding code to SDKView
Create a custom UIView and name it SDKView.
Swift
import UIKit
class SDKView:UIView{
Add these variables:
@objc var onWorkoutDidFinish: RCTDirectEventBlock?
@objc var onDidExitWorkout: RCTDirectEventBlock?
@objc var exercisesArray:NSArray = []
@objc var soundData:NSDictionary = [:]
let startSDK:([WorkoutExercise], WorkoutAudioVideoFiles?)->Void
onWorkoutDidFinish: an event that will tell the react-native code that the workout is done.
onDidExitWorkout: an event that will tell the react-native code that the user chose to exit the workout.
exercisesArray: this array is the exercise data
soundData: this dictionary will hold the raw sound data
startWorkout: a callback for the direct workout start button.
we will need two functions that will translate exercisesArray and startWorkout:
//Translate exercisesArray to array of WorkoutExercise
func getExercises() -> [WorkoutExercise]{
var exercises:[WorkoutExercise] = []
exercisesArray.forEach { element in
if let dic = element as? [String: Any]{
var uiElements:[CustomUIElements] = []
if let rawUIElements = dic["uiElements"] as? [String]{
rawUIElements.forEach { string in
if let element = CustomUIElements(rawValue: string){
uiElements.append(element)
}
}
}
exercises.append(
WorkoutExercise(
instruction: URL(string: dic["instruction"] as? String ?? ""),
totalSeconds: dic["totalSeconds"] as? Int,
totalReps: dic["totalReps"] as? Int,
customUIElements: uiElements,
type: CustomActivityUnit(rawValue: dic["type"] as? String ?? "-"))
)
}
}
return exercises
}
//Translate soundData NSDictionary WorkoutAudioVideoFiles
func getWorkoutAudioVideoFiles()->WorkoutAudioVideoFiles?{
if soundData.count > 0{
return WorkoutAudioVideoFiles(
introduction: URL(string: soundData["introduction"] as? String ?? ""),
soundtrack: URL(string: soundData["soundtrack"] as? String ?? ""),
phoneCalVideoURL: URL(string: soundData["phoneCalVideoURL"] as? String ?? ""),
phoneCalAudioURL: URL(string: soundData["phoneCalAudioURL"] as? String ?? ""),
bodyCalStartAudioURL: URL(string: soundData["bodyCalStartAudioURL"] as? String ?? ""),
bodyCalFinishedAudioURL: URL(string: soundData["bodyCalFinishedAudioURL"] as? String ?? "")
)
}
return nil
}
Now, we'll add a start button:
lazy var startButton:UIButton = {
let button = UIButton()
button.setTitle("START WORKOUT", for: .normal)
button.setTitleColor(.black, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(startWorkoutButtonTapped), for: .touchUpInside)
return button
}()
@objc func startWorkoutButtonTapped(){
let exercises = getExercises()
let workoutAudioVideoFiles = getWorkoutAudioVideoFiles()
startSDK(exercises,workoutAudioVideoFiles)
}
Finally, let's add everything to the initializer:
init(startSDK: @escaping ([WorkoutExercise], WorkoutAudioVideoFiles?)->Void){
self.startSDK = startSDK
super.init(frame: .zero)
self.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: centerXAnchor),
button.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
2.4 Adding the controller and implementing the SDK
Create SDKViewController.swift the same way you created the SDKView
Add these imports and create the custom class:
import UIKit
import SencyMotionLib
class SDKViewController: UIViewController {
And now let us add the SDKView we created and other class members
@objc lazy var sdkView = SDKView(startSDK: startWorkout)
var workoutInfo:WorkoutInfo?
lazy var sencyMotion = SencyMotion()
override func loadView() {
view = sdkView
}
Implement the startWorkout method
func startWorkout(exercises:[WorkoutExercise], workoutAudioVideoFiles: WorkoutAudioVideoFiles?){
let workouts = CustomWorkoutBuilder(exercises: exercises, languge: .Spanish, workoutAudioVideoFiles: workoutAudioVideoFiles)
do{
try sencyMotion.startWorkout(viewController: self, customWorkoutBuilder: workouts, delgate: self)
}catch{
print(error)
}
}
Custom design for 'phone moved' alert
Notice that -
is used to implement a custom design of your choice.
It's important to remember to implement all delegate methods to control all callbacks.
Everything looks good but one thing is missing - the delegate implementation, so at the end of the file add the following code:
Swift
extension SDKViewController:SencyMotionDelegate{
func workoutDidFinish(summary: SencyMotionLib.WorkoutSummary) {
if let workoutDidFinish = sdkView.onWorkoutDidFinish {
workoutDidFinish(["summary":summary.toJson()])
}
}
func didExitWorkout(summary: SencyMotionLib.WorkoutSummary) {
if let didExitWorkout = sdkView.onDidExitWorkout{
didExitWorkout(["summary":summary.toJson()])
}
}
func bodyAssessmentTestDidFinish(result: SencyMotionLib.BodyAssessmentResult) {}
func didExitTheTest() {}
func workoutDidFail(error: Error) {
if let onErrorRecived = sdkView.onErrorRecived{
onErrorRecived(["error": error.localizedDescription])
}
}
}
What we are doing in workoutDidFinish and didExitWorkout is checking that we add in our react-native and then calling it with summary Json.
2.6 Finally let's create the react-native bridge
To do so we will create a new file, but instead of choosing Swift File, choose Objective-C File and name it SDKViewManager.m.
Now let us add some code :
#import "React/RCTViewManager.h"
@interface RCT_EXTERN_MODULE(SDKViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(exercisesArray, NSArray)
RCT_EXPORT_VIEW_PROPERTY(soundData, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(onWorkoutDidFinish, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDidExitWorkout, RCTDirectEventBlock)
@end
here are two things that are important to notice here:
RCT_EXTERN_MODULE: this will expose the manager. The first argument is the manager's name and the second one is the type.
RCT_EXPORT_VIEW_PROPERTY: this will expose our variables like the RCT_EXTERN_MODULE the first argument is the variable name name and the second is the type
And we are done with Xcode it's time for react-native
3. Adding to react native
Sample implementation. of App.js to use the SDK as it is implemented in the example above
import React, { useState, useRef } from 'react';
import { StyleSheet, requireNativeComponent, Alert, Modal, View, NativeModules, Text, Pressable } from 'react-native';
const MyViewController = requireNativeComponent('SDKView');
const MyNativeModule = NativeModules.SDKViewManager;
const ViewStyleProps = () => {
const [modalVisible, setModalVisible] = useState(false);
const [workoutInfo, setWorkoutInfo] = useState('NO INFO');
var jsonObject = null;
const didExitWorkout = (e) => {
console.log(e.nativeEvent.summery);
console.log('didExitWorkout');
};
const workoutDidFinish = (e) => {
console.log(e.summery);
console.log('workoutDidFinish');
};
const workoutDidFail = (e) => {
console.log(e.error);
console.log('workoutDidFail');
};
const data = [
{
instruction:"URL",
totalSeconds: 20,
totalReps: 10,
uiElements: ["gaugeOfMotion","repsCounter","timer"],
type: "HighKnees"
},
{
instruction:"URL",
totalSeconds: 20,
totalReps: 10,
uiElements: ["gaugeOfMotion","repsCounter","timer"],
type: "Jumps"
}];
const soundData = {
introduction:"(optinal) URL",
soundtrack: "(optinal) URL",
phoneCalVideoURL:"(optinal) URL",
phoneCalAudioURL:"(optinal) URL",
bodyCalStartAudioURL:"(optinal) URL",
bodyCalFinishedAudioURL:"(optinal) URL"
};
return (
<MyViewController
style={styles.sdk}
exercisesArray={ data }
soundData = {soundData}
onWorkoutDidFinish={workoutDidFinish}
onDidExitWorkout={didExitWorkout}
onErrorRecived={workoutDidFail}
>
<Modal
animationType="fade"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}>{workoutInfo}</Text>
<View style={[ styles.container,{ flexDirection: 'row', gap: 50}]}>
<Pressable
style={[styles.button, styles.buttonBack]}
onPress={() => setModalVisible(!modalVisible)}>
<Text style={styles.textStyle}>Back</Text>
</Pressable>
<Pressable
style={[styles.button, styles.buttonStart]}
onPress={() => {
MyNativeModule.startWorkoutFromInfo()
setModalVisible(!modalVisible)
}}>
<Text style={styles.textStyle}>Start</Text>
</Pressable>
</View>
</View>
</View>
</Modal>
</MyViewController>
);
};
const styles = StyleSheet.create({
description: {
fontSize: 18,
textAlign: 'center',
color: '#656565',
marginTop: 65,
},
sdk:{
flex: 1,
alignItems: "center",
justifyContent: "center"
},
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginTop: 22,
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 20,
padding: 35,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
button: {
padding: 20,
elevation: 2,
},
buttonOpen: {
backgroundColor: '#F194FF',
},
buttonStart: {
flex:2,
backgroundColor: '#2196F3',
},
buttonBack:{
flex:2,
backgroundColor: '#820000'
},
textStyle: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
},
modalText: {
marginBottom: 15,
textAlign: 'left',
},
});
const App = () => {
return ViewStyleProps();
};
export default App;
First import requireNativeComponent
Then you will need to get SDKViewManger as you can see when you call, javascript automatically omits 'Manger', and react-native ignores it
Create the callbacks for the events we created
It's time to call the view as you can see I am also setting the variables we created in the view
And you are done! Run the app and you should be able to start the SDK.