resqnet's blog

技術的なことか、Amazonアソシエイト・プログラムの参加者です

DesignSystemへの理解と困った事

これはなに?

Alla Kholmatova さんが書かれた本。
DesignSystem を読んで自分の理解をまとめつつ、運用して感じたことなどを残す。

DesignSystemとは

標準の定義があるわけではない。

「スタイルガイド」「パターンライブラリ」などと同じ意味で使われる場合もあるが、一貫性を持って編成された一連の繋がったパターン、また共有されたプラクティスのセット。

パターンはボタン、テキストフィールド、アイコン、タイポグラフィなどの様々なインターフェイスを構成します。

基本的に以下の要素で構成されています。

  • デザイン原則
  • 機能パターン
  • 認知パターン
  • 共通言語

デザイン原則とは

インターフェースをデザインするにあたり、プロダクトの目的とエートスが明確であることが重要です。

目的を踏まえあらゆる決定をする必要があり、これらの価値観にプロダクトに関わる人の同意と、コミットが必要となります。

そのために基礎となる価値と原則を確立させる必要があります。

作成時点で何もなければ、会社のビジョンやプロダクトビジョン、大きなビジョンにデザイン原則がどう貢献できるかを考えるところから始めるのが良い。

目的や対象を絞り原則を進化させていくことでパターン化していきます。

原則はデザインパターンに限らずデザイン原則にそってサイトのパフォーマンスや運営方法などにも表現されていきますが。

チームでの合意が不可欠です。

機能パターン

インターフェースの具体的な構成要素です。
特定のユーザ行動を可能にしたり促したりすることを目的としています。

ユーザの行動を基にどのようにデザインするかを考える必要があります。

この本では「10分レシピサイト」でわかりやすく例えられていました。

ユーザー行動の例です。 「レシピを選ぶ」「割り当てられた時間内に手順を進める」などがあります。

これらのユーザ行動を基に機能パターンをどの様にデザインするかを考えます。

機能パターンはシンプルでも良いですし、いくつか組み合わせて複雑なパターンにしても構わないです。

プロダクトが進化するとパターンも進化していきます。ユーザがレシピを採点できるようにして、その点数をレシピカードに表示させるかもしれません。

大事なのはパターンのテストをなんども繰り返し、より効果的にユーザー行動を促せるようにすることです。

「パターンは進化、振る舞いは不動です」

機能パターンを作成するのにカスタマージャーニーなどを用いることがあります。

認知パターン

オブジェクトの組み合わせにより雰囲気で認知が変わるという性質を持つ。

例えば部屋の雰囲気は人によってだいぶ違います。 家具次第では家に見えたり、倉庫に見えたりもします。

デジタルプロダクトに置ける認知パターンは、 トーン、タイポグラフィ、カラーパレット、レイアウト、イラスト、アイコンスタイル、形状、テクスチャなど様々な要素をインターフェースで組み合わせて使用する多様な方法が含まれます。

認知パターンは意図的では無い場合でも常に存在しています。

これらはプロダクトでスタイルやスキンなどで取り上げられるが認知パターンは表面上のものではなく、ブランドのコアに存在してこそ本領を発揮します。

モジュール式のシステムでは一貫性のあるシームレスな外見にするのが難しい場合があります。 認知パターンが浸透することでそれらをつなぎ合わせることが可能です。

共有言語

デジタルプロダクトではチームで構築していきます。

メンバーはそれぞれ異なる目標とスケジュールがあります。 そのためいい加減なパターンが追加されたりモジュールが重複することは避けられません。

大勢の人が関わっている中で一貫性や一体感を持つには、 チームが DesignSystem とその仕組みについて共通理解・認識を持つ必要があります。

チームが同じ原則に従っていることブランドビジョンが揃っていることです。一貫した DesignSystem には共有される言語が必要です。

そのためにはパターンランゲージのアプローチが有効です。 しかし実現するには多大な労力が必要になる場合があります。

機能させるために必要な取り決めやプラクティス

  • 命名規則のパターンを決める
  • チームで命名する
  • 専用チャンネルを準備する( Slack など)
  • デザインランゲージを浸透させる
  • オブジェクトをそれぞれの名称で呼ぶ
  • プロジェクトの導入研修に取り入れる
  • 定期的な DesignSystem キャッチアップを実施する
  • 用語集の維持

実践して困ったこと

デザイン原則が曖昧なまま進行するUIライブラリ集

既存のアプリケーションに DesignSystem を導入を進めた際、UIライブラリ集や一部の認知パターンを導入していくことはできた。特に最初のリリースまでは問題なく進行していたと思う。しかしデザイン原則が曖昧なまま運用を続けてしまい少しづつ歯車が噛み合わない箇所が発生してしまった。特にカラーの命名周りで発生していた様に思う。

今回、改めて勉強し直しデザイン原則の大切さを理解できた。

ドメインに引っ張られるUIライブラリ集

DesignSystem として認知パターンなどを実装していくことはできたがアプリケーションのドメインに引っ張られる実装などが見受けられた。例えば汎用性の無いAPI通信ロジックなどでこれらは DesignSystem には不要だと思うが、開発が進むにつれて増えるエッジケースの対応などに困った。

これに関しては議論の中で「 ApplicationAware かどうか?」という切り分けがとても良かった。

「 Application-aware かどうか」とは 「プロジェクト」や「ユーザー」といった概念 (=ドメイン) に依存するかどうか 「内包するカード状のUI部品を並べるUI部品」と「ユーザの画像を表示するUI部品」という2つのUI部品があった時、前者はカード状のUI部品という他のUI部品に依存するのに対し、後者は「ユーザ」のデータに依存する。ドメインに依存するものはデータが必要、と置き換えて考えることもできる

既存プロジェクトでデザイン原則を構築する

デザイン原則とは何か? DesignSystem に必要なことは何か?がしっかりと認知されていないチームで実装が進む事により産まれる歪み。またプロダクトのグロース・修正とデザイン原則作成タスクの優先順位付けなどがスムーズに行かない。

まとめ

DesignSystem 実践前に一読し、実践後改めて読み直し理解を深めて見落としていた事などが多かったなと感じつつも、 DesignSystem や実装にあたり周辺技術の恩恵はかなりあったと思う。運用や浸透に課外が起きがちなのはある程度どの方法でも起きる気はするので、しばらく運用を続けて勉強していきたい所存。

React+Cypress+StorybookでVisualRegressionTestする

はじめに

React+CypressでVisualRegressionを実現するのにやったこと

環境構築

react/storybook導入

npx create-react-app my-app
npx -p @storybook/cli sb init

Cypress導入

yarn add cypress @testing-library/cypress -D

// 不要なテストを削除
rm -rf cypress/integration/examples

Storybookのpreview-iframeを取得

Storybookのiframeを取得する

cypress/support/command.js

import '@testing-library/cypress/add-commands';

Cypress.Commands.add('getIframeBody', () => {
  cy.log('getIframeBody');

  return cy
      .get('#storybook-preview-iframe')
      .its('0.contentDocument.body').should('not.be.empty')
      .then((body) => cy.wrap(body))
});

Cypress-VisualRegressionモジュールを追加

良さげだったのでこちらを利用させていただいた https://www.npmjs.com/package/cypress-visual-regression

yarn add cypress-visual-regression -D

設定を追加する
cypress/support/command.js

const compareSnapshotCommand = require('cypress-visual-regression/dist/command');
compareSnapshotCommand();

cypress/plugin/index.js

const getCompareSnapshotsPlugin = require('cypress-visual-regression/dist/plugin');

module.exports = (on) => {
  getCompareSnapshotsPlugin(on);
};

cypress.json

{
  "screenshotsFolder": "./cypress/snapshots/actual",
  "trashAssetsBeforeRuns": true
}

テスト用コンポーネント準備

Storyookのデフォルトコンポーネントを利用

src/stories/Button.js

export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
  const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  const [buttonLabel, setLabel] = React.useState(label);
  const onClick = () => { setLabel("hoge"); }

  return (
    <button
      type="button"
      className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
      style={backgroundColor && { backgroundColor }}
      data-testid="button"
      {...props}
      onClick={onClick}
    >
      {buttonLabel}
    </button>
  );
};

buttonのテストを追加

cypress/integration/button_spec.js

describe('Button Component', () => {
  beforeEach(() => {
    cy.visit('http://localhost:6006/?path=/story/example-button--primary');
  });

  it('Button Click', () => {
    cy.getIframeBody().findByTestId('button').should('have.text', 'Button');
    cy.getIframeBody().findByTestId('button').click().should('have.text', 'hoge');
  });

  it('should display the Button correctly', () => {
    cy.getIframeBody().findByTestId('button').contains('Button');
    cy.compareSnapshot('button');
  });
});

PackageにScriptを追加

package.json

"scripts" : {
    "cypress:base": "cypress run --env type=base --config screenshotsFolder=cypress/snapshots/base",
    "cypress:run": "cypress run --env type=actual",
    "cypress:open": "cypress open"
}

テストの実行

// Baseの作成
yarn cypress:base

// テストを実行
yarn cypress:run

結果

Base f:id:resqnet:20200904111917p:plain Actual f:id:resqnet:20200904111913p:plain Diff f:id:resqnet:20200904111922p:plain

TestをTypeScriptで実行する

ルートのtscofing.jsonに下記を追加

  "include": [
    "node_modules/cypress/types/*.ts",
    "cypress/*/*.ts"
  ]

サンプルリポジトリ

github.com

XCodeでMacのストレージ がいっぱい

モチベーション

XCodeMacのストレージ がいっぱいになってしまったのでその備忘録

 

コマンド備忘録

  • 容量確認したい時のコマンド
    du -sh ~/Library/*  
  • とりあえず困ったら消しておけば大丈夫な人たち
sudo rm -rf ~/Library/Developer/Xcode/DerivedData/*
sudo rm -rf ~/Library/Developer/Xcode/Archives/*
sudo rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/*

おおよそこれで解決するはず

StorybookをNetlifyへホスティングする

tl;dr

StorybookをNetlifyへホスティングする備忘録とRailsでハマったことのメモ

まずはApplicationを準備

npx create-react-app storybook-netlify --typescript
cd storybook-netlify
yarn add -D @storybook/cli
yarn sb init

Netlifyのビルド設定

f:id:resqnet:20200612111146p:plain

PRを作成する

git checkout -b feature/button2
cp 1-Button.stories.js 2-Button.stories.js

// コピーして新たなストーリー名をつける
vi 2-Button.stories.js
git add 2-Button.stories.js
git commit -m 'add button2'
git push

CIでNetlifyへDeployされる

f:id:resqnet:20200612111159p:plain

Button2が追加されていることが確認できる

f:id:resqnet:20200612111215p:plain

今回使った環境

https://github.com/resqnet/StorybookNetlifySample

その他 Tips

Railsと共存する環境でハマったこと

NetlifyではRoot上のGemfileやPackage.jsonを自動でinstallするようです。

そのためStorybookのみをホスティングしたいが、Gemfileを読み込みRuby versionでの互換性や不要なGemのinstallなどが発生しました。

この問題を解決するためBase directoryを一時フォルダにしBuild commandでカレントを移動することで対応できます。

Base directory: ./app
Build command: cd ..; yarn install; yarn build-storybook
Publish directory: ./app//..publish

ちなみにNetlifyの環境変数など確認したのですが、該当の設定は見つからなかったので見落としてたら、教えてほしいです:pray:

Unity2D ボタン式のスイッチ実装

tl;dr

ボタン式のスイッチを実装する際にinterfaceを利用すると簡単に作れる、という話です

モチベーション

ボタンを押すと何かが動く、そんなギミックを作る事は多々あるかと思うのですが、 スイッチやボタンで検索すると「特定の動作専用のボタン」みたいなのが多く(先人に感謝)、「汎用的に使えるスイッチ」みたいな実装がなかったので書く

ひとまずよくあるスイッチを実装

f:id:resqnet:20200609180816p:plain ※円で囲った石がスイッチなのである(手抜き)

まずはスイッチ(石)を踏むと隣にある木を飛ばす

public class SwitchObject : MonoBehaviour
{
  public GameObject EffectObject;

  private void OnCollisionEnter2D(Collision2D other)
  {
    EffectObject.GetComponent<Wood>().OnSwitch();
  }
}
public class Wood : MonoBehaviour
{
  bool Depature = false;
  private void Update()
  {
    if (Depature)
    {
      transform.Translate(Vector3.up * Time.deltaTime * 5, Space.World);
    }
  }

  public void OnSwitch()
  {
    Depature = true;
  }
}

EffectObjectにwoodを設定して確認 f:id:resqnet:20200609181103p:plain

飛んでいく

次は左の岩も飛ばそう

まずは木からコピペ

public class Stone : MonoBehaviour
{
  bool Depature = false;
  private void Update()
  {
    if (Depature)
    {
     transform.Translate(Vector3.up * Time.deltaTime * 5, Space.World);
    }
  }
  
  public void OnSwitch()
  {
    Depature = true;
  }
}
// 飛ばすオブジェクトはStoneになる
EffectObject.GetComponent<Stone>().OnSwitch();

f:id:resqnet:20200609180821p:plain なんでも飛んでいく

汎用的にしたい

これだとSwitchをたくさん作らないといけないので大変になる。

Switchで押されるオブジェクトは共通してOnSwitch()を持っている。OnSwitchが実装されたSwitchならば共通して扱いたい。interfaceで実装していこう。

interface ISwitch {
  void OnSwitch();
}

public class SwitchObject : MonoBehaviour
{
  public GameObject EffectObject;

  private void OnCollisionEnter2D(Collision2D other)
  {
    EffectObject.GetComponent<ISwitch>().OnSwitch();
  }
}

Stone/Woodにinterfaceを持たせる

// ISwitchを持たせる
public class Wood : MonoBehaviour, ISwitch
{
  bool Depature = false;
  private void Update()
  {
    if (Depature)
    {
      transform.Translate(Vector3.up * Time.deltaTime * 5, Space.World);
    }
  }

  public void OnSwitch()
  {
    Depature = true;
  }
}

これでStoneだろうがWoodだろうがなんだろうがISwitchさえ持っていればOnSwitchを利用できる 配列にすることで同時に扱える

public class SwitchObject : MonoBehaviour
{
  public GameObject[] EffectObjects;

  private void OnCollisionEnter2D(Collision2D other)
  {
    foreach(GameObject gObj in EffectObjects){
      gObj.GetComponent<ISwitch>().OnSwitch();
    }
  }
}

おわり