React Native - iOS custom workout
  • 31 Jan 2024
  • 7 Minutes to read
  • Dark
    Light

React Native - iOS custom workout

  • Dark
    Light

Article Summary

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:

  1. Let Cocoapods know about the source URL's

  2. Tells Cocoapods to use dynamic libraries

  3. Add SencyMotionLib pod

  4. Comment these lines.

  5. 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:

  1. RCT_EXTERN_MODULE: this will expose the manager. The first argument is the manager's name and the second one is the type.

  2. 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;
  1.  First import requireNativeComponent

  2.  Then you will need to get SDKViewManger as you can see when you call, javascript automatically omits 'Manger', and react-native ignores it

  3.  Create the callbacks for the events we created

  4.  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.


Was this article helpful?