Figma上のアイコンとExpo(React Native)を連携し、型安全に利用する
この記事はFigma 開発 Advent Calendar 2022の20日目の記事です。
はじめに
この記事は、下記の方法について紹介します。
- Figmaでアイコンをデザイン
- Expoプロジェクトに1コマンドで取り込み、自動的にコンポーネント(.tsx)化
もともとは、Expo公式で紹介されている方法を参照していましたが、アイコンが意図せずに塗りつぶされてしまい、この事象を解決出来ませんでした。
そのため、次のようなアプローチでアイコンを表示するようにしました。
- Figmaでアイコンを作成
- Figma APIでSVGを取得
- SVGOで圧縮
- SVGR+react-native-svgでReact Native用のコンポーネントを自動生成
この内容について、紹介していきます。
Figmaでアイコンを作る
初めに、FigmaでDesign Fileを作成します。
アイコンは、paddingや一貫性を意識しつつ作成します。
一貫性においては、特にstrokeの太さ、角丸の大きさ、グリッド・キーフレームを考慮すると良さそうです。
以下の記事・ガイドラインが参考になります。
アイコンが出来たら、以下の操作を行います。
- Union Selection
- Flatten Selection (⌘+E)
- Outline stroke (Shift+⌘+O)
- ConstraintsをScaleへ
詳しくは、下記の記事でも紹介されています。
最後にコンポーネント化します(例では、playアイコンが完成)。
LibraryをPublishします(以後、更新するたびにUpdateが必要)
最後に、後続の作業で必要なFigmaのAccessTokenとFile IDをメモします。
AccessTokenは、Figmaホームの右上のアイコンからSettings→AccountタブのPersonal access tokenから生成&コピー出来ます。File IDは、Design FileのコピーリンクのURL内から取得できます。
このあたりは、下記の記事にも詳しく書いてあります。
これで、Figma側の準備は終了です。
Expoプロジェクトで取得と書き出しを自動化する
FigmaのSVGデータを取得
以下のコマンドで作成したプロジェクトに対し、Figmaアイコンを書き出していきます。
// Choose a template: › blank (TypeScript)
expo init .
Figmaのアイコンを取得するために必要なライブラリをaddします。
yarn add -D got
yarn add -D request
PJ直下に、以下のファイルを作成します。
下記コードはこの記事を参考にさせて頂きました。
import got from 'got';
import request from 'request';
import { resolve } from 'path';
import { createWriteStream } from 'fs';
const TOKEN = process.env.FIGMA_TOKEN;
const FIGMA_FILE_KEY = process.env.FIGMA_FILE_KEY;
const download = (url, path, callback) => {
request.head(url, (err, res, body) => {
request(url).pipe(createWriteStream(path)).on('close', callback);
});
};
async function main() {
const { body } = await got(
`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`,
{
headers: {
'X-FIGMA-TOKEN': TOKEN,
},
responseType: 'json',
},
);
const results = body.meta.components;
const ids = results.map((r) => r.node_id).join(',');
const {
body: { images },
} = await got(
`https://api.figma.com/v1/images/${FIGMA_FILE_KEY}?ids=${ids}&format=svg`,
{
headers: {
'X-FIGMA-TOKEN': TOKEN,
},
responseType: 'json',
},
);
const nodeIds = Object.keys(images);
for (const nodeId of nodeIds) {
const url = images[nodeId];
const result = results.find((r) => r.node_id === nodeId);
const name = result.name;
const path = resolve(`assets/svgs/${name}.svg`);
download(url, path, () => {
console.log(url, path);
});
}
}
main();
また、assets配下にsvgsディレクトリを作成します(svg書き出し先)。
package.jsonに下記を追記します。
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"gen:icon": "node genFigmaIcons.mjs" //ここ
},
yarn gen:icon
実行前に環境変数を設定します。※環境変数の追加は.envなどお好きな選択肢をご利用ください。
// コマンドラインで下記を実行
export FIGMA_TOKEN="自分のAccessToken"
export FIGMA_FILE_KEY="アイコンが追加されたDesignFileのID"
そして、yarn gen:icon
をコマンドラインで実行します。
Done in 2.60s.
のように表示され、/assets/svgs配下にsvgが吐き出されていればOKです。
SVGOで圧縮、加工
Figmaから取得した、SVGを圧縮、一部属性を削除するためにSVGOをインストールします。
yarn add -D svgo
以下の設定ファイルをPJ直下に追加します。
module.exports = {
multipass: false,
plugins: [
{
name: 'removeXMLNS',
params: {
removeXMLNS: true,
},
},
],
};
package.json
の"gen:icon"
コマンドにsvgoに関するコマンドを追記します。
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"gen:icon": "node genFigmaIcons.mjs && npx svgo --config svgo.config.js ./assets/svgs"
},
yarn gen:icon
を行って、svgが下記画像のように改行がなければ成功です。
SVGRでReactNative対応のコンポーネントを書き出し
そのために、SVGRとreact-native-svgをインストールします。
yarn add -D @svgr/cli
expo install react-native-svg
次にsvgr用の設定ファイルをPJ直下に作成します。
module.exports = {
typescript: true,
native: true,
outDir: 'components/icons',
replaceAttrValues: { '#000': '{props.color}' },
svgProps: { viewBox: '0 0 24 24' },
};
package.json
に、以下のように追記し、yarn gen:icon
をコマンドラインで実行します。
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"gen:icon": "node genFigmaIcons.mjs && npx svgo --config svgo.config.js ./assets/svgs && npx @svgr/cli --svgo-config .svgrrc.js --no-index ./assets/svgs"
},
すると、/components/icons直下にPlay.tsxが作成されているはずです。
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
const SvgPlay = (props: SvgProps) => (
<Svg width={24} height={24} fill="none" viewBox="0 0 24 24" {...props}>
<Path d="M10 15.464 16 12l-6-3.464v6.928Z" fill={props.color} />
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z"
fill={props.color}
/>
</Svg>
);
export default SvgPlay;
Iconコンポーネントを作り、効率的にアイコンを利用できるようにする。
最後に書き出した、Play.tsxやその後作成したアイコンを一括管理できるように、Icon.tsxを以下のように作成します。
import React, { FC } from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import SvgPlay from './Play';
export type IconTypeProps =
| 'play'
| '';
export interface IconProps {
type: IconTypeProps;
size: number;
color: string;
style?: StyleProp<ViewStyle>;
}
const Icon: FC<IconProps> = (props) => {
switch (props.type) {
case 'play':
return (
<SvgPlay
width={props.size}
height={props.size}
color={props.color}
style={props.style}
/>
);
default:
return (
<SvgPlay
width={props.size}
height={props.size}
color={props.color}
style={props.style}
/>
);
}
};
export default Icon;
実際に利用するときは、以下のように利用します。
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import Icon from './components/icons/Icon';
export default function App() {
return (
<View style={styles.container}>
<View style={styles.helloIcons}>
<Icon type="play" size={24} color="#000" style={styles.icon}/>
<Text>Open up App.tsx to start working on your app!</Text>
</View>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
helloIcons: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
icon: {
marginRight: 4
}
});
これにて、説明は終了です。
課題
SVGOを使った理由としては、svgrの設定がうまく効かず、xmls属性の削除が出来なかったからです。xmls属性は、tsx化したときに型エラーを引き起こします。そのため、svgoを前段に噛まして、xmls属性の削除を行っています。
そして、Iconのprops周りもまだ改善できそうです。
また、最後のIcon.tsxもtemplate等を利用して自動生成できると思うので、今後やってみたいと思います。
おわりに
ReactNativeは最近触り初めたので、もっと良い方法があるかも...?と思いながら書いたので、より良い方法があれば教えて下さい
また、普段以下のようなものを作っているので、よければ見てやってください。
Next.js + Figma token
Next.js + React Lottie + Adobe AfterEffect
Next.js + D3.js + Python(データ整形)
Discussion