📱

Figma上のアイコンとExpo(React Native)を連携し、型安全に利用する

2022/12/20に公開

この記事はFigma 開発 Advent Calendar 2022の20日目の記事です。

はじめに

この記事は、下記の方法について紹介します。

  1. Figmaでアイコンをデザイン
  2. Expoプロジェクトに1コマンドで取り込み、自動的にコンポーネント(.tsx)化

もともとは、Expo公式で紹介されている方法を参照していましたが、アイコンが意図せずに塗りつぶされてしまい、この事象を解決出来ませんでした。

そのため、次のようなアプローチでアイコンを表示するようにしました。

  • Figmaでアイコンを作成
  • Figma APIでSVGを取得
  • SVGOで圧縮
  • SVGR+react-native-svgでReact Native用のコンポーネントを自動生成

https://github.com/Nomura0118/figma-icon

この内容について、紹介していきます。

Figmaでアイコンを作る

初めに、FigmaでDesign Fileを作成します。

アイコンは、paddingや一貫性を意識しつつ作成します。
一貫性においては、特にstrokeの太さ、角丸の大きさ、グリッド・キーフレームを考慮すると良さそうです。
以下の記事・ガイドラインが参考になります。
https://uxdesign.cc/7-principles-of-icon-design-e7187539e4a2
https://m3.material.io/styles/icons/designing-icons

アイコンが出来たら、以下の操作を行います。

  • Union Selection
  • Flatten Selection (⌘+E)
  • Outline stroke (Shift+⌘+O)
  • ConstraintsをScaleへ

詳しくは、下記の記事でも紹介されています。
https://zenn.dev/amon/articles/3e3fd786d73e80

最後にコンポーネント化します(例では、playアイコンが完成)。

LibraryをPublishします(以後、更新するたびにUpdateが必要)

最後に、後続の作業で必要なFigmaのAccessTokenとFile IDをメモします。
AccessTokenは、Figmaホームの右上のアイコンからSettings→AccountタブのPersonal access tokenから生成&コピー出来ます。File IDは、Design FileのコピーリンクのURL内から取得できます。

このあたりは、下記の記事にも詳しく書いてあります。
https://zenn.dev/seya/articles/924aadf933034d#トークンを取得する

これで、Figma側の準備は終了です。

Expoプロジェクトで取得と書き出しを自動化する

FigmaのSVGデータを取得

以下のコマンドで作成したプロジェクトに対し、Figmaアイコンを書き出していきます。

// Choose a template: › blank (TypeScript)
expo init .

Figmaのアイコンを取得するために必要なライブラリをaddします。

yarn add -D got
yarn add -D request

PJ直下に、以下のファイルを作成します。
下記コードはこの記事を参考にさせて頂きました。

/genFigmaIcons.mjs
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に下記を追記します。

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で圧縮、加工

https://github.com/svg/svgo/
Figmaから取得した、SVGを圧縮、一部属性を削除するためにSVGOをインストールします。

yarn add -D svgo

以下の設定ファイルをPJ直下に追加します。

svgo.config.js
module.exports = {
  multipass: false,
  plugins: [
    {
      name: 'removeXMLNS',
      params: {
        removeXMLNS: true,
      },
    },
  ],
};

package.json"gen:icon"コマンドにsvgoに関するコマンドを追記します。

package.json
"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対応のコンポーネントを書き出し

https://docs.expo.dev/ui-programming/using-svgs/
https://react-svgr.com/docs/cli/
整形済みのsvgからReactNative対応のコンポーネントを生成するために、SVGRを利用します。
そのために、SVGRとreact-native-svgをインストールします。

yarn add -D @svgr/cli
expo install react-native-svg

次にsvgr用の設定ファイルをPJ直下に作成します。

.svgrrc.js
module.exports = {
  typescript: true,
  native: true,
  outDir: 'components/icons',
  replaceAttrValues: { '#000': '{props.color}' },
  svgProps: { viewBox: '0 0 24 24' },
};

package.jsonに、以下のように追記し、yarn gen:iconをコマンドラインで実行します。

package.json
"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が作成されているはずです。

/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を以下のように作成します。

/components/icons/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;

実際に利用するときは、以下のように利用します。

App.tsx
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
https://gotodiving.vercel.app/

Next.js + React Lottie + Adobe AfterEffect
https://mau-sc-motion.vercel.app/

Next.js + D3.js + Python(データ整形)
https://seawater-temperature-data-visualization.vercel.app/

Discussion