Multi Scan

You can utilize the `startFoodDetection` API to scan food items through the device Camera.

How can I implement the startFoodDetection API and retrieve food items?

/**
     * Begin food detection using the device's camera.
     * @param options - An object to determine which types of scanning should be performed.
     * @param callback - A callback to repeatedly receive food detection events as they occur.
     * @returns A `Subscription` that should be retained by the caller while food detection is running. Call `remove` on the subscription to terminate food detection.
     */
    
useEffect(() => {
    // Configuration for food detection
    const config: FoodDetectionConfig = {
      detectBarcodes: true,
      detectPackagedFood: true,
      detectNutritionFacts: false,
    };

    // Start food detection and subscribe to events
    const subscription = PassioSDK.startFoodDetection(
      config,
      async (detection: FoodDetectionEvent) => {
        const { candidates } = detection;

        // Check if there are candidates with relevant information
        if (
          candidates &&
          ((candidates.detectedCandidates &&
            candidates.detectedCandidates.length > 0) ||
            (candidates.packagedFoodCode &&
              candidates.packagedFoodCode.length > 0) ||
            (candidates.barcodeCandidates &&
              candidates.barcodeCandidates.length > 0))
        ) {
          // Get attributes for the detected food candidates
          // This method is mentioned below in utility method
          const attributes = await getAttributesForFoodCandidates(candidates);

          // Update passioIDAttributes state
          setPassioIDAttributes((food) => {
            const copyOfPassioIDAttributes: PassioIDAttributes[] = [];

            // Filter out duplicate entries
            attributes.forEach((value) => {
              if (food.every((item) => item.passioID !== value.passioID)) {
                copyOfPassioIDAttributes.push(value);
              }
            });

            // If there are new attributes, update the state
            if (copyOfPassioIDAttributes.length > 0) {
              return [...food, ...copyOfPassioIDAttributes];
            } else {
              return food;
            }
          });

          setIsLoading(false);
        }
      }
    );

  

    // Cleanup function to unsubscribe when the component unmounts
    return () => subscription.remove();
  }, []);

Example

Require @gorhom/bottom-sheet for bottom sheet

In this , we establish the useMultiscan hook, which furnishes methods and data such as food, and use of DetectionCameraView

useMultiScanning

import {
  type FoodDetectionConfig,
  type FoodDetectionEvent,
  type PassioIDAttributes,
  PassioSDK,
} from '@passiolife/nutritionai-react-native-sdk-v2';
import { getAttributesForFoodCandidates } from '../../utils';
import { useEffect, useState, useCallback } from 'react';

/**
 * Custom hook for handling multi-scanning using PassioSDK.
 * It provides functions and state variables related to the detection of multiple food items.
 */
export function useMultiScanning() {
  // State variables
  const [isLoading, setIsLoading] = useState(true);
  const [passioIDAttributes, setPassioIDAttributes] = useState<
    PassioIDAttributes[]
  >([]);

  useEffect(() => {
    // Configuration for food detection
    const config: FoodDetectionConfig = {
      detectBarcodes: true,
      detectPackagedFood: true,
      detectNutritionFacts: false,
    };

    // Start food detection and subscribe to events
    const subscription = PassioSDK.startFoodDetection(
      config,
      async (detection: FoodDetectionEvent) => {
        const { candidates } = detection;

        // Check if there are candidates with relevant information
        if (
          candidates &&
          ((candidates.detectedCandidates &&
            candidates.detectedCandidates.length > 0) ||
            (candidates.packagedFoodCode &&
              candidates.packagedFoodCode.length > 0) ||
            (candidates.barcodeCandidates &&
              candidates.barcodeCandidates.length > 0))
        ) {
          // Get attributes for the detected food candidates
           // This method is mentioned below in utility method
          const attributes = await getAttributesForFoodCandidates(candidates);

          // Update passioIDAttributes state
          setPassioIDAttributes((food) => {
            const copyOfPassioIDAttributes: PassioIDAttributes[] = [];

            // Filter out duplicate entries
            attributes.forEach((value) => {
              if (food.every((item) => item.passioID !== value.passioID)) {
                copyOfPassioIDAttributes.push(value);
              }
            });

            // If there are new attributes, update the state
            if (copyOfPassioIDAttributes.length > 0) {
              return [...food, ...copyOfPassioIDAttributes];
            } else {
              return food;
            }
          });

          setIsLoading(false);
        }
      }
    );


    // Cleanup function to unsubscribe when the component unmounts
    return () => subscription.remove();
  }, []);

  // Function to delete a food item by its passioID
  const onDeleteFoodItem = useCallback((passioID: string) => {
    setPassioIDAttributes((prev) =>
      prev.filter((value) => value.passioID !== passioID)
    );
  }, []);

  // Return the hook's public API
  return {
    isLoading,
    passioIDAttributes,
    onDeleteFoodItem,
  };
}

MultiScan

import { ActivityIndicator, View } from 'react-native';
import {
  blackBackgroundStyle,
  bottomSheet,
  cameraStyle,
} from './MultiScanningStyle';

import BottomSheet from '@gorhom/bottom-sheet';
import { DetectionCameraView } from '@passiolife/nutritionai-react-native-sdk-v2';
import React, { useMemo, useRef } from 'react';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { useMultiScanning } from './hooks/useMultiScanning';
import { MultiScanResult } from './MultiScanResult';
export const MultiScan = gestureHandlerRootHOC(() => {
  const { isLoading, passioIDAttributes, onDeleteFoodItem } =
    useMultiScanning();

  const bottomSheetModalRef = useRef<BottomSheet>(null);

  const snapPoints = useMemo(() => ['20%', '75%'], []);

  return (
    <View style={blackBackgroundStyle}>
      <DetectionCameraView style={cameraStyle} />
      <BottomSheet
        ref={bottomSheetModalRef}
        index={0}
        snapPoints={snapPoints}
        backgroundStyle={bottomSheet.backgroundStyle}
        enableContentPanningGesture={
          isLoading || passioIDAttributes.length <= 3
        }
      >
        {isLoading || passioIDAttributes.length === 0 ? (
          <ActivityIndicator />
        ) : (
          <View style={[bottomSheet.container]}>
            <MultiScanResult
              attributes={passioIDAttributes}
              onClearResultPress={onDeleteFoodItem}
            />
          </View>
        )}
      </BottomSheet>
    </View>
  );
});

MultiScanResult

import { StyleSheet } from 'react-native';

import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { type PassioIDAttributes } from '@passiolife/nutritionai-react-native-sdk-v2';
import React from 'react';
import { MultiScanResultItem } from './MultiScanResultItem';

export interface MultiScanResultProps {
  attributes: PassioIDAttributes[];
  onClearResultPress: (item: PassioIDAttributes) => void;
}

export const MultiScanResult = ({
  attributes,
  onClearResultPress,
}: MultiScanResultProps) => {
  const styles = multiScanResultStyle();

  const renderItem = ({ item }: { item: PassioIDAttributes }) => {
    return (
      <MultiScanResultItem
        attribute={item}
        onClearResultPress={onClearResultPress}
      />
    );
  };

  return (
    <BottomSheetFlatList
      contentContainerStyle={styles.flatList}
      data={attributes}
      renderItem={renderItem}
      keyExtractor={(item) => item.passioID}
      extraData={attributes}
    />
  );
};

const multiScanResultStyle = () =>
  StyleSheet.create({
    flatList: {
      marginVertical: 8,
      paddingBottom: 250,
    },
  });

MultiScanResultItem

import React from 'react';
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';

import {
  IconSize,
  PassioIconView,
  type PassioIDAttributes,
} from '@passiolife/nutritionai-react-native-sdk-v2';
import AlternativeFood from '../quick/AlternativeFood';

export interface MultiScanResultItemProps {
  attribute: PassioIDAttributes;
  onClearResultPress?: (item: PassioIDAttributes) => void;
}

const MultiScanResultItem = ({
  attribute,
  onClearResultPress,
}: MultiScanResultItemProps) => {
  const styles = quickScanStyle();

  const onAlternativeFoodItemChange = () => {};

  return (
    <View style={styles.itemContainer}>
      <View style={styles.foodResult}>
        <PassioIconView
          style={styles.itemIcon}
          config={{
            passioID: attribute.imageName ?? attribute.passioID,
            iconSize: IconSize.PX180,
            passioIDEntityType: attribute.entityType,
          }}
        />
        <Text style={styles.itemFoodName}>{attribute.name}</Text>
      </View>
      <AlternativeFood
        onAlternativeFoodItemChange={onAlternativeFoodItemChange}
        passioId={attribute.passioID}
      />
      <Pressable
        style={styles.clearResult}
        onPress={() => onClearResultPress?.(attribute)}
      >
        <Image
          resizeMode="contain"
          style={styles.clearResultItem}
          source={require('../../../assets/ic_close_white.png')}
        />
      </Pressable>
    </View>
  );
};

const quickScanStyle = () =>
  StyleSheet.create({
    itemContainer: {
      padding: 12,
      marginHorizontal: 16,
      backgroundColor: 'white',
      marginTop: 16,
      left: 0,
    },
    foodResult: {
      flexDirection: 'row',
      alignItems: 'center',
    },
    itemFoodName: {
      flex: 1,
      textTransform: 'capitalize',
      marginHorizontal: 8,
      fontSize: 16,
    },
    itemIcon: {
      height: 60,
      width: 60,
    },
    clearResult: {
      position: 'absolute',
      top: -9,
      backgroundColor: 'blue',
      overflow: 'hidden',
      padding: 4,
      height: 26,
      width: 26,
      justifyContent: 'center',
      borderRadius: 30,
      alignItems: 'center',
      right: 0,
    },
    clearResultItem: {
      height: 8,
      width: 8,
    },
  });
export default React.memo(MultiScanResultItem);

AlternativeFood

import React, { useState, useEffect } from 'react';
import { FlatList, Pressable, StyleSheet, Text } from 'react-native';
import {
  IconSize,
  PassioIconView,
  type PassioID,
  type PassioIDAttributes,
} from '@passiolife/nutritionai-react-native-sdk-v2';
import { getAlternateFoodItems, getAttributesForPassioID } from '../utils';

interface AlternativeFoodProps {
  passioId: PassioID;
  onAlternativeFoodItemChange?: (item: PassioIDAttributes) => void;
}

const AlternativeFood = React.memo(
  ({ passioId, onAlternativeFoodItemChange }: AlternativeFoodProps) => {
    const [alternativeAttributes, setAlternativeAttributes] =
      useState<PassioIDAttributes[]>();
    const styles = alternativeFoodStyle();

    useEffect(() => {
      console.log('alternative called ----');
      async function init() {
        const passioAttributeID = await getAttributesForPassioID(passioId);
        if (passioAttributeID != null) {
          const list = await getAlternateFoodItems(passioAttributeID);
          setAlternativeAttributes(list);
        }
      }
      init();
    }, [passioId]);

    if (alternativeAttributes?.length === 0) {
      return null;
    }

    return (
      <FlatList
        horizontal
        data={alternativeAttributes}
        renderItem={({ item }) => {
          return (
            <Pressable
              onPress={() => onAlternativeFoodItemChange?.(item)}
              style={styles.itemContainer}
            >
              <PassioIconView
                style={styles.itemIcon}
                config={{
                  passioID: item.imageName ?? item.passioID,
                  iconSize: IconSize.PX180,
                  passioIDEntityType: item.entityType,
                }}
              />
              <Text style={styles.itemFoodName}>{item.name}</Text>
            </Pressable>
          );
        }}
        keyExtractor={(item) => item.passioID.toString()}
      />
    );
  }
);

const alternativeFoodStyle = () =>
  StyleSheet.create({
    itemContainer: {
      marginHorizontal: 8,
      alignItems: 'center',
      marginVertical: 16,
      flexDirection: 'row',
    },
    itemFoodName: {
      marginHorizontal: 8,
      flex: 1,
      textTransform: 'capitalize',
      fontSize: 12,
    },
    itemIcon: {
      height: 24,
      width: 24,
    },
  });

export default AlternativeFood;

Utils Methods

getAttributesForFoodCandidates

export async function getAttributesForFoodCandidates({
  detectedCandidates,
  barcodeCandidates,
  packagedFoodCode,
}: FoodCandidates): Promise<PassioIDAttributes[]> {
  const detectionCandidateAttributes = detectedCandidates?.map(
    ({ passioID }) => {
      return PassioSDK.getAttributesForPassioID(passioID);
    }
  );
  const barcodeCandidatesAttributes = barcodeCandidates?.map(({ barcode }) => {
    return PassioSDK.fetchAttributesForBarcode(barcode);
  });

  const ocrCandidatesAttributes = packagedFoodCode?.map((ocrCode) => {
    return PassioSDK.fetchPassioIDAttributesForPackagedFood(ocrCode);
  });
  const attrs = await Promise.all([
    ...(barcodeCandidatesAttributes ?? []),
    ...(ocrCandidatesAttributes ?? []),
    ...detectionCandidateAttributes,
  ]);
  return attrs.filter(notEmpty);
}

getAlternateFoodItems


export async function getAlternateFoodItems(
  passioIDAttributes: PassioIDAttributes
): Promise<PassioIDAttributes[]> {
  const childrenAttributes = passioIDAttributes.children.map(
    async ({ passioID }) => {
      return PassioSDK.getAttributesForPassioID(passioID);
    }
  );
  const siblingAttributes = passioIDAttributes.siblings.map(
    async ({ passioID }) => {
      return PassioSDK.getAttributesForPassioID(passioID);
    }
  );
  const parentAttributes = passioIDAttributes.parents.map(
    async ({ passioID }) => {
      return PassioSDK.getAttributesForPassioID(passioID);
    }
  );

  const combinedAttributes = [
    ...childrenAttributes,
    ...siblingAttributes,
    ...parentAttributes,
  ];

  const attrs = await Promise.all(combinedAttributes);
  return attrs.filter(notEmpty);
}

getAttributesForPassioID

export async function getAttributesForPassioID(
  passioID: PassioID
): Promise<PassioIDAttributes | null> {
  return await PassioSDK.getAttributesForPassioID(passioID);
}

Yeah. Added Multi Scan.

Result

Last updated