Yukashikadoでは、React Nativeを使って、iOSとAndroidとモバイルアプリを開発しています。 皆さんは、figmaからモバイルアプリにデザインを落とし込むときに、端末に応じて、スタイルをかき分けて大変な思いをしたことがあるでしょうか?? 今回は、OSやデバイスの大きさに応じたスタイル分岐をせずに、一度だけスタイルを書けば完了するStyleの書き方について紹介します
課題感
figmaのスタイルを実装時に変更するというのは、 figmaの見た目とモバイルの見た目に不一致があるためです。 その発生パターンは以下のおおよそ2つパターンだと思います。
- figmaと実装時の利用デバイスの画面幅の不一致によるスタイルの崩れ
- 実装後、複数の異なる画面サイズやOSで確認したときにスタイルの崩れ
figmaと実装時の利用デバイスの画面幅の不一致
figma上では、画面幅を決めてデザインが作成されます。 Yukashikadoのfigma上では、モバイルのディスプレイ幅が375pxとして、デザインが作成されています。 開発時に使用している端末が、横幅のdipが375(iPhoneXなど)であれば、figmaの画面と実際の画面の大きさが一致します。 しかし、開発時に利用している端末の横幅のdipが異なれば、 figma上の1pxと画面上の1dipが一致しません。 何も考えずに作業をすると、 そこに発生するズレを、見ながら修正する作業が発生します。
ちなみにどれぐらい違うかというと、同じiPhoneでもこれだけ違います
iPhoneX: 375x812, iPhone15 Pro Max: 430x932 参考
実装後、複数の異なる画面サイズやOSで確認したときにスタイルの崩れ
モバイル端末の大きさは様々なことから、 iPhoneXで作っていい感じだった画面が、iPhone15 Pro Maxで見るといい感じではないようなことがおきます。 同じOSでも問題が発生してますが、AndroidとiOSではそれがより顕著に発生します。 画面サイズに加えて、 AndroidとiOSでデフォルトで使えるフォントが異なることから、それによって、見栄えが大きく異なることがあります。
どうやって解決するのか
以下の手順で解決できると思います。
- 基準となるデバイスを決める
- OSごとのフォントの方針をきめる
- 基準デバイスと実デバイスの差分をスケーリングさせる実装をする
1.基準となるデバイスを決める
デザイナーと開発者で、基準となるデバイスを決めます。 Yukashikadoのfigma上では、モバイルのディスプレイ幅が375pxとして、デザインが作成されているので、iPhoneXで375x812を基準としています。
2. OSごとのフォントの方針をきめる
OSごとにフォントの方針を決めます。 iOSは多種多様なデフォルトフォントが揃えられています 参考 しかし、Androidはrobotoとnotoがデフォルトで使えるフォントとして用意されていますが、ほかのフォントは機種によって異なります。 カスタムフォントを使うことによって、OSのデフォルトのフォントに依存しないやり方もあります。 yukashikadoでは、ユーザがおよそ8割がiPhoneユーザであることから、基本はiPhoneのフォントでfigmaを組み、iPhoneベースで実装し、Androidについてはシステムフォントを利用するという方針を取りました。 Androidでフォントによってデザインが崩れるのは、一旦妥協することにしています。 また、具体的なコードなどについは今回は割愛します。
3. 基準デバイスと実デバイスの差分をスケーリングさせる実装をする
ここからは具体的なやり方です。 差分をスケーリングするには、react-native-size-mattersというライブラリを使います。 このライブラリのありがたみをきちんと理解したい方は、この記事を一読するのをおすすめします。 このライブラリがやってくれることしてはは、dipを実デバイスと基準デザインに応じて、スケーリングできます。 たとえば、
- (基準デバイス) iPhoneXで375x812
- (実デバイス) iPhone15 Pro Maxで、430x932
としたときに、 15dipは、横幅をもとに、iPhoneXでは15dipですが、iPhone15 Pro Maxでは、17.14dip(15 * 430 / 375)として扱うことができます。
縦幅でスケールすることもできます。また、スケールの適応具合も調整することもできます。 個人的には、横幅をもとにスケールすることが正しくスケールできるのでそちらをおすすめします。
使い方は簡単で、StyleSheetの代わりに、ScaledSheetを使うことで、スケーリングされた値を使うことができます。 @sをつけることで、横幅をもとにスケーリングされます。 @vsをつけることで、縦幅をもとにスケーリングされます。 @msrをつけることで、丸めてくれます。
const styles = ScaledSheet.create({
container: {
width: '100@s', // = scale(100)
height: '200@vs', // = verticalScale(200)
padding: '2@msr', // = Math.round(moderateScale(2))
margin: 5
},
row: {
padding: '10@ms0.3', // = moderateScale(10, 0.3)
width: '50@ms', // = moderateScale(50)
height: '30@mvs0.3' // = moderateVerticalScale(30, 0.3)
}
});
ただこのまま使うにはいくつか課題が残ります。 react-native-size-mattersは、350x680を基準デバイスにしています。 それは、ソースコード上に書き換えられています。 なので、patch-packageで、以下の値を決めた基準端末に書き換えてください。 lib/scaling-utils.js
const guidelineBaseWidth = 350; const guidelineBaseHeight = 680;
このまま使うと、iOSよりAndroidのほうが少しフォントサイズが大きいので、Androidで見たときに、文字がはみ出てたり、違和感が残ります。 なので、androidのフォントのときは、0.5dipさげてからスケーリングすることで、見た目を調整をおすすめします。
最後に使い心地の課題です。 通常のStyleSheet.createであれば、100と書けばいいところを、'100@s'と書く必要があり、普通にめんどくさいです。 flexやzIndexなどの画面の縦横以外の意味を持つプロパティは、のぞいて、すべてスケーリングしたいです。 なので、そのあたりを解決したStyleの作成関数を作ります。以下のとおりです。
import {
NamedStyles,
scale,
ScaledSheet,
Size,
} from 'react-native-size-matters'
import { Dimensions, Platform, RegisteredStyle } from 'react-native'
/**
* スケールを適用させたくないプロパティ
* これら以外は、100と指定したときに、`100@s`としてスケーリングされる
*/
const excludedProperties = [
'aspectRatio',
'flex',
'flexGrow',
'flexShrink',
'flexBasis',
'zIndex',
'opacity',
]
/**
* フォントで特殊対応しなければ行けないプロパティ
*/
const fontScaleProperties = ['fontSize', 'lineHeight']
/**
* プラットフォームの差を吸収する
*/
const fontDiff =
Platform.select({
ios: 0,
android: -0.5, // Androidのほうが大きくなるので、小さくする
}) || 0
/**
* スタイルシートの型定義。styles.xxxから正しく補完させる
*/
type TStyleSheet<T> = {
[P in keyof T]: RegisteredStyle<{
[S in keyof T[P]]: T[P][S] extends Size ? number : T[P][S]
}>
}
/**
* 標準デバイスに合わせてスケーリングしたStyleを作成する
* @param stylesObject
*/
export const autoScaleStyles = <
T extends NamedStyles<T> | NamedStyles<Record<string, unknown>>,
>(
stylesObject: T,
): TStyleSheet<T> => {
const scaledObject: Record<string, Record<string, unknown>> = {}
// 各スタイルオブジェクトを取得して、ループする
for (const styleName in stylesObject) {
scaledObject[styleName] = {}
// 各プロパティを取得して、ループする
for (const prop in stylesObject[styleName]) {
const value = stylesObject[styleName][prop]
// 数字で、かつ、スケール対象のプロパティ
const isScaleProperty =
!excludedProperties.includes(prop) && typeof value === 'number'
if (isScaleProperty) {
if (fontScaleProperties.includes(prop)) {
// フォントの差分を吸収する
scaledObject[styleName][prop] = scale(value + fontDiff) // Androidで、lineHeightが@sできかない
} else {
// 数字の場合、@sを追加
scaledObject[styleName][prop] = `${value}@s`
}
} else {
// スケール対象ではないのでそのまま代入
scaledObject[styleName][prop] = value
}
}
}
return ScaledSheet.create<T>(scaledObject as T)
}
上記のコードをつかうことで、以下のように、styles.top_imageはスケール済みの値として使うことができます。
const styles = autoScaleStyles({
top_image: {
width: 100,
height: 100,
},
})
結果
別の画面ですが、AndroidとiOSでこのような画面になりました。 AndroidとiOSでフォントが異なるので違う感じがしますが、おおよそいい感じがしています。
最後に
ReactNativeで、デバイスやOSに依存しないスタイルの書き方について紹介しました。
figmaから、スタイルを一度書くだけで済むようになったので、だいぶスタイルが楽になったと思います。
もっとより良いやり方があれば、また記事を書きたいと思います。
ありがとうございました。