memoirする画面の共有機能を実装したので解説
Pull Request
実装
内容としては、memoirする画面(1週間分のタスクを表示する)をスクリーンショットして、共有できるような感じで実装していきます。

まず、画面スクリーンショットはreact-native-view-shotを使用して実装
以下みたいな感じで実装できます
import React, { useRef } from 'react';
import ViewShot from 'react-native-view-shot';
import { View, Text } from 'react-native';
export default () => {
const viewShot = useRef(null);
const onCapture = () => {
const url = await viewShot.current.capture();
console.log(url);
}
return (
<ViewShot onCapture={onCapture} ref={viewShot} options={{ format: 'jpg' }}>
<Text>テスト</Text>
</ViewShot>
)
}
ViewShot でラップした部分をキャプチャーすることが可能です。
この画像を、expo-sharingを使用して画像を共有します
先程の例に追記すると、こんな感じで実装できます
import React, { useRef } from 'react';
import ViewShot from 'react-native-view-shot';
import { View, Text } from 'react-native';
import * as Sharing from 'expo-sharing';
export default () => {
const viewShot = useRef(null);
const onCapture = async() => {
const url = await viewShot.current.capture();
const ok = await Sharing.isAvailableAsync();
if (ok) {
await Sharing.shareAsync('file://' + url);
}
}
return (
<ViewShot onCapture={onCapture} ref={viewShot} options={{ format: 'jpg' }}>
<Text>テスト</Text>
</ViewShot>
)
}
この2つのライブラリを使用して実装していきます。 実装する中で以下の対応を行いました。
- ①.元々がインフィニティスクロールしている画面だったので、スクリーンショット用に、全項目表示の画面を作成
- ②.レンダリングから画像が表示されるまでのタイムラグがあるので、全ての画像表示が完了したらキャプチャーするように実装
①の方は、サクッと別画面として全項目表示用の画面を実装
■ src/components/organisms/Memoir/ScreenShot.tsx
import React, { memo, useRef, useCallback, useState } from 'react';
import {
StyleSheet,
ScrollView,
useWindowDimensions,
Alert,
} from 'react-native';
import * as Sharing from 'expo-sharing';
import ViewShot from 'react-native-view-shot';
import View from 'components/atoms/View';
import { useNavigation } from '@react-navigation/native';
import dayjs from 'lib/dayjs';
import theme from 'config/theme';
import { Props as PlainProps } from 'components/pages/Memoir/ScreenShot/Plain';
import { User as TUser } from 'store/atoms';
import Header from 'components/molecules/Memoir/Header';
import { getModeCountMax } from 'lib/utility';
import DateText from 'components/molecules/Memoir/DateText';
import Divider from 'components/atoms/Divider';
import Loading from 'components/molecules/Overlay/Loading';
import { Item } from 'hooks/useItemsInPeriodPaging';
import Card from './Card';
export type Props = Pick<PlainProps, 'users'> & {
startDate: string;
endDate: string;
items: Item[];
};
type User = Omit<TUser, 'userID'> & {
id: string;
};
type Card = Item & {
user: User;
};
type RenderedItem = {
date: string | null;
contents?: Card;
categoryID?: number;
last?: boolean;
width: number;
onLoadEnd: () => void;
};
const RenderItem: React.FC<RenderedItem> = (props) => {
if (props.date) {
return <DateText date={props.date} categoryID={Number(props.categoryID)} />;
}
return (
<View>
<View mb={3} mx={3}>
<Card
title={props?.contents?.title || ''}
categoryID={props?.contents?.categoryID || 0}
user={props?.contents?.user as User}
onPress={() => null}
onLoadEnd={props.onLoadEnd}
/>
{!props?.last && <Divider />}
</View>
</View>
);
};
const ScreenShot: React.FC<Props> = (props) => {
const viewShot = useRef<ViewShot>(null);
const navigation = useNavigation();
const count = useRef(0);
const [loading, setLoading] = useState(true);
const windowWidth = useWindowDimensions().width;
const onShare = useCallback(
async (uri: string) => {
const ok = await Sharing.isAvailableAsync();
if (!ok) {
Alert.alert('エラー', '共有機能を利用できませんでした', [
{
text: '戻る',
onPress: () => {
navigation.goBack();
},
},
]);
return;
}
await Sharing.shareAsync(uri);
navigation.goBack();
},
[navigation]
);
const onCapture = useCallback(async () => {
const url = await viewShot.current?.capture?.();
if (url) onShare('file://' + url);
}, [onShare]);
const onLoadEnd = useCallback(() => {
count.current = count.current + 1;
if (props.items.length === count.current) {
setLoading(false);
setTimeout(() => onCapture(), 100);
}
}, [props.items, onCapture]);
const dates = Array.from(
new Set(props.items.map((v) => dayjs(v.date).format('YYYY-MM-DD')))
);
const dateItems = dates.sort().map((date) => {
const contents = date;
return {
date,
contents,
};
});
const data = dateItems
.map((v1) => {
const sameDateItems = props.items.filter(
(v2) => dayjs(v2.date).format('YYYY-MM-DD') === v1.date
);
const item: RenderedItem[] = sameDateItems.map((v2, index) => {
const user: User | undefined = props.users.find(
(v) => v.id === v2.userID
);
return {
date: null,
contents: {
...v2,
user: user || {
id: '',
displayName: '',
image: '',
},
},
last: sameDateItems.length === index + 1,
width: windowWidth,
onLoadEnd: onLoadEnd,
};
});
const categoryID = item.map((v) => Number(v.contents?.categoryID));
const dateItem: RenderedItem = {
date: v1.date,
categoryID: getModeCountMax(categoryID),
width: windowWidth,
onLoadEnd: () => null,
};
return [dateItem, ...item];
})
.flat();
return (
<>
<ScrollView style={styles.root}>
<ViewShot ref={viewShot} options={{ format: 'jpg' }}>
<Header startDate={props.startDate} endDate={props.endDate} />
{data.map((v, index) => (
<RenderItem {...v} key={index} />
))}
<View />
</ViewShot>
</ScrollView>
{loading && <Loading text="作成中" />}
</>
);
};
const styles = StyleSheet.create({
root: {
backgroundColor: theme().color.background.main,
flex: 1,
width: '100%',
paddingTop: theme().space(4),
},
});
export default memo(ScreenShot);
①はReact NativeのImageのonLoadEndから表示完了タイミングを取得できるので、こちらをハンドリングして、全ての画像が表示されたタイミングでキャプチャーするように実装
これで諸々の実装が完了、動作は以下のようになりました