はじめまして。SocialDogの高橋(@sofpyon)です。2021年10月頃までスマホアプリを開発するチーム(社内では「Nativeチーム」と呼んでいます)に所属していました。
さて、2021年8月、ダークモードに対応したSocialDogアプリ(バージョン3.0.0)をリリースしました。本記事では、どのような工程を経てSocialDogアプリをダークモードに対応させることができたのか、紹介したいと思います。
本記事では概念的な説明が中心になります。SocialDogにおけるダークモード対応の方法が今後変化していく可能性があること、またReact Nativeは進化のスピードが速く、本記事の情報が陳腐化してしまう恐れがあることから、React Nativeのバージョンや利用している技術スタックに依存しない考え方のみを紹介します。
ダークモード対応についてもっと技術に踏み込んだことを聞いてみたい!と思った方は、ぜひWantedlyでお気軽にご連絡ください。
SocialDogアプリの開発に利用している主な技術
- React Native
- TypeScript
- styled-components
ダークモード対応のきっかけ
(画像は SocialDog Tech Blog をはじめます! | SocialDog Tech Blog より)
iOS 13 / Android 10 の新機能としてダークモード(ダークテーマ)が登場し、話題になりました。iOS 13 / Android 10 リリース当初、SocialDogアプリとしてはダークモードに対応していませんでした。当時、ダークモードに対応したアプリはまだ少なかったように思います。
時は流れ、最近ではダークモードに対応するアプリが増えてきました。 多くのアプリがダークモードに対応する中、SocialDogアプリのみダークモード非対応となると、ダークモードを好んで使うユーザーには悪印象を持たれかねません。
各社のアプリがダークモードに対応し始めると、逆に対応していないアプリが目立つようになってきます。なぜならダークモードに対応していない(特に白背景の)アプリは眩しく感じるからです。これはダークモードを愛用している人からするとかなりのマイナスイメージです。
—— DarkModeのデザインを中心とした色彩設計の考え方 - くらげだらけより
その頃、私個人の趣味での開発において、React Nativeアプリのダークモード対応を行なっていました。この経験によってダークモード対応のイメージを把握していたこともあり、SocialDogでもアプリのダークモード対応が始まりました。
ダークモード対応の前に立ちはだかる壁
アプリをダークモード対応させるには、ライトモード時の色に対応するダークモード時の色を定義する必要があります(例えば、ライトモードで「白色」の背景は、ダークモードでは「黒色」にするなど)。
ただ、SocialDogアプリでの「色の管理方法」には問題がありました。SocialDogでは、アプリ内で利用される色は定数化し、1ファイルにまとめていました。ダークモード対応以前、このファイルは下記のようになっていました。
(図: ダークモードに対応する以前の色定数管理ファイル)
このファイルに定義されている定数(色定数)の数に圧倒されます。それぞれの色定数に明確な用途があるならば、数の多さは問題ではありません。このファイルは、下記のような問題がありました。
問題点1: 似たような色が重複定義されていた
カラーコード自体は異なるが、見た目上の違いがほとんど無い色が複数定義されていました。また、どの定数をどの箇所に使うべきかといった共通認識がチーム内にありませんでした。
(図: 色々な灰色)
問題点2: 色定数名と実際に使われるべき箇所が一致していない場合があった
(アイコンフォントの色)のように、それぞれの色が使われるべき箇所が定数名で示されています。上記の画像を見ると COLOR_BG_
で始まる定数(背景色)、COLOR_TEXT_
(文字色)、COLOR_BORDER_
(ボーダー色)、COLOR_ICON_
(アイコンフォントの色)のように、それぞれの色が使われるべき箇所が定数名で示されています。
しかし実際には、「background-color
の指定に COLOR_BORDER_
」といったケースが一定数ありました。色定数の使い方に関して明文化されたルールが特に無かったため、Pull Requestのレビュー時、色定数の使い方に関する議論はあまり無かったと思います。
問題点3: 定数を使わずカラーコードをハードコードしている箇所があった
例えば background-color: #fff;
というコードが随所に見られました。 また、アプリの一部の画面でしか使われない色はカラーコードを直接指定している箇所が多くありました。アプリ内で利用されている色を一括変更したい場合、色定数ファイルの変更のみでは対応できないという状況でした。
ダークモード対応の流れ
「ダークモード対応」というのは、「ライトモード時の色に対応するダークモード時の色を決める作業」と言っても過言ではありません。
ダークモード対応を実施する以前は、上記で見てきたように「ダークモード対応前に使っている色=ライトモード時の色」が不必要に多く定義されている状態でした。これらの色1つ1つについてダークモード時の色を決めていくのは徒労です。
そこで、ダークモード対応を始める前に、現状定義されている色定数を整理・削減していくところから始めました。
1. 色定数を整理・削減
(図: 色定数一覧をHTMLで出力)
まずは現状定義されている色定数を全て把握します。
定義されている色定数の全容を簡単に把握できるよう、色定数一覧をHTMLファイルに出力できるようにしました。このHTMLファイルを見つつ、「灰色系」「青系」「緑系」……という順番になるよう、色定数の順番を並べ替えました。
次に、似ている色を削除し、本当に必要な色だけが残るように色定数の整理を行いました。
(図: 色の整理)
2. ダークモード対応のための仕組みを作成
ダークモード対応後も残す色を決めた後、次にダークモード対応のための基盤を作成していきました。この際、React Nativeやstyled-componentsにある次のような機能を活用しました。
- React NativeのuseColorSchemeフック
- このReactフックを使うと、ユーザーの端末設定が「ライトモード」か「ダークモード」かを判別できます。ただ、ダークモードに対応途中のコードもメインブランチにマージしていきたいため、このフックをカスタムフックでラップしています。本番環境では強制的にライトモードとみなすカスタムフックを実装しました。
- styled-componentsのTheming機能
- Theming機能を利用することで、「ライトモード時の色」と「ダークモード時の色」をそれぞれ定義することができます。この機能と前述したカスタムフックを組み合わせることで、「ライトモード時の色」と「ダークモード時の色」を出し分けることができます。
また、Theming機能で定義した色をコード内で簡単に呼び出せるよう、下記の3つの仕組みを整備しました。
① styledColor関数: styled-componentsのCSS内で色を呼び出す
styled-componentsで定義したテーマを呼び出すコードは、標準だと若干書く量が多く辛いです。
// ...
export const GreenBox = styled.button`;
background-color: ${props => props.theme.green};
`;
そこで、 styledColor
関数を作成して上記の処理をラップするようにしました。
// ...
import { styledColor } from 'path/to/styledColor';
export const GreenBox = styled.button`
background-color: ${styledColor('green')};
`;
② useAppThemeフック: 関数コンポーネント内から色を呼び出す
時には、styled-componetsのスタイル内ではなく、関数コンポーネント内で色を使いたいことがあります。例えばReact NativeのTouchableHighlightコンポーネントでは、ボタン押下時の色を指定するには underlayColor
というPropにカラーコードを渡す必要があります。
そういったケースに対応するため、関数コンポーネント内からstyled-componentsのテーマを参照できる useAppTheme
というカスタムフックを作成しました。
// ...
import { useAppTheme } from 'path/to/useAppTheme';
import { styledColor } from 'path/to/styledColor';
export const Button: React.FC = () => {
// useAppTheme フックが返す appColor は関数
const { appColor } = useAppTheme();
return (
// underlayColor prop に渡す値を appColor 関数で作成
<StyledButton underlayColor={appColor('primary')}>
<Text>ボタン</Text>
</StyledButton>
);
}
const StyledButton = styled.TouchableHighlight`
background-color: ${styledColor('surface')};
`;
③ AppThemeConsumerコンポーネント: クラスコンポーネント内から色を呼び出す
SocialDogアプリの開発は、まだクラスコンポーネントの利用が主流だった時代から始まっています。現在でも多くのコンポーネントがクラスコンポーネントとなっています。
そのため、関数コンポーネントだけでなくクラスコンポーネント内からもstyled-componentsのテーマを参照できるようにする必要があります。
そこで、下記のような AppThemeConsumer
というコンポーネント(render prop)を作成し、クラスコンポーネントでも色を呼び出せるようにしました。
// ...
import { AppThemeConsumer } from 'path/to/AppThemeConsumer';
export class PenIcon extends PureComponent {
render() {
return (
// ↓ここ
<AppThemeConsumer>
{({ appColor }) => (
<Icon
icon="pen"
size={32}
color={appColor('primary')}
/>
)}
</AppThemeConsumer>
);
}
}
3. ダークモードとライトモードの配色の対応を決める
ライトモード時の色に対応するダークモード時の色を定義する仕組みは、「2. ダークモード対応のための仕組みを作成」においてすでに作成済みです。ただし、まだダークモード時の色は決めていません。ダークモード時の色は、ライトモード時の色と同じ色を仮置きしている状態です。
ダークモード対応の仕組みは揃ったので、次はダークモード時の色を決めていく作業を行いました。私自身は、当時エンジニア兼デザイナーといった感じの役割でした。そのため、ダークモードの配色決定も行いました。
配色を決めるにあたり、主にマテリアルデザインにおけるダークテーマの考え方を大いに参考にしました。
この工程は、次の「4. 古い色定数を使っている箇所を新しい仕組みに置き換える」と同時並行で行いました。
(図: Dark theme - Material Design より。SocialDogアプリのダークモード対応にあたって、上記の命名を参考にしました)
4. 古い色定数を使っている箇所を新しい仕組みに置き換える
いよいよ実際の画面をダークモードに対応していきます。
この工程は、機械的に文字列置換していくような作業がほとんどになります。そのため、チームの他の方にも協力を仰ぎ、少しずつダークテーマ対応を進めました。正直地味な作業ではあるのですが、1画面丸ごとダークモードに対応した時の達成感はひとしおです。
ちなみに、新しく定義すべき配色がこの工程で見つかることがあるため、「3.」と「4.」の作業は行ったり来たりします。
5. リリース
SocialDogでは、ダークモード対応がほぼ完了したタイミングで、ダークモード対応バージョンをリリースしました。
ここまでの作業は、ユーザーが必ず目にするであろう箇所は必ずダークモードに対応した状態にすることを目指していました。部分部分でダークモードに対応していない箇所があると、出来が悪いアプリに見えるためです。
しかし完璧な状態を目指していると、いつまでもダークモード対応は完了しません。そのため、特殊な条件を満たさないと表示されないような画面については、ダークモードに対応していなくても許容することにしていました。今でも、ダークモード化していない画面を見つけ次第、対応を行っている状況です。
※ 分析系機能については、Web版SocialDogの画面をそのまま表示している関係上、ダークモードに対応していません。
色の使い方をドキュメント化
なお、ダークモード対応と同時に、色の使い方のドキュメント化を行いました。前述した「問題点2: 色定数名と実際に使われるべき箇所が一致していない場合があった」を解消するためです。また、社内LT会においてこのドキュメントの内容を簡単に紹介しました。
(図: 社内LT会で利用したスライドの一部)
まとめ
SocialDogアプリにおけるダークモード対応の工程を一通りご紹介しました。
React Nativeを利用したアプリ開発が気になる、話してみたいなと思った方は、ぜひWantedlyでお気軽にご連絡ください!