iOSでアイコンのメンテナンスを自動化する方法

# はじめに

先日、[SARAH](https://apps.apple.com/jp/app/id916083595)というグルメアプリの、ダークモード対応を担当しました🎉

その際に、大量のアイコンをメンテナンスする必要があったので、Xcodeへのiconアセット取り込みを自動化するスクリプトを作りました。
ZeplinからエクスポートしたPDFに癖があり、一致を調べるのが案外難しかったので、参考になれば幸いです。(実は、imagemagick便利、という話です。)

# 課題

- ZeplinのどのアイコンがLightとDarkのペアなのか分からない👀
- 手で一つ一つXcodeに登録していくのは、めちゃ時間がかかる😭
  - アイコンがアップデートされた時に、また大変😱
- 人手で行うと、ミスが起こるリスクがある

# 自動化の方針

- 同じアイコンのLight/Dark違いは、必ず同じファイル名にしてもらう
- Zeplinから一括でダウンロードする
- スクリプトで`*.imageset`と`Contents.json`を自動生成する

# 自動化の課題

ZeplinからエクスポートしたPDFは、レンダリング結果が同じでも、バイナリレベルでは毎回違うことが判明しました。(メタデータ？)
そうすると以下のような事になり、あまり美しくありません。

- 変更されたアイコンが少なくても、gitのログだと全て変更されたように見える
- light/darkが同じアイコンなのに、余分なPDFが登録される

# 工夫した点

2つのPDFの内容がピクセルレベルで同じかどうかを、`imagemagick`で判別するようにしました。
いつの間にか、PDFにも標準で対応してたんですね〜
最初は単純にググって`diff-pdf`とかを使ってましたが、依存ライブラリが大量な上に、結果はイマイチでした。

- lightとdarkのPDFの内容が同じだった場合、anyのみで登録
- 前回インポートしたPDFと内容が同じだった場合、copyしない
  - gitのログで、何のアイコンが更新されたかが分かる！

参考: [ImageMagickで2枚の画像を比較](https://qiita.com/kwst/items/c40817b3cdf841995257)

# スクリプト

shellで関数とか分岐をこんなに書いたのは初めてかも…😅

参考: [割りと便利だけど微妙に忘れがちなbashのコマンド・チートシート](https://qiita.com/jpshadowapps/items/d6f9b55026637519347f)

- `~/Downloads/light/`にダウンロード
- `~/Downloads/dark/`にダウンロード
- `./scripts/import-icons.sh`で一括インポート

`compare`の出力画像は捨てて、終了コードだけを利用しています。

```sh
#!/usr/bin/env sh

brew install imagemagick

light_dir=~/Downloads/light
dark_dir=~/Downloads/dark
assets_dir=SARAH/Resources/Icons.xcassets
tmpdir=$(mktemp -d)
trap "rm -rf $tmpdir" 0 2

# compare by pixel
function is_identical() {
  if [ ! $# = 2 ] ; then
    echo "is_identical(): Invalid arguments: $*"
    exit 1
  fi
  if [ ! -f $1 -o ! -f $2 ] ; then
    return 1
  fi
  compare -metric AE $1 $2 /dev/null >& /dev/null
  return $?
}

# copy only if not identical
function overwrite() {
  file=`basename $2`
  if is_identical $1 $2 ; then
    echo "Skip: $file"
    return
  fi
  echo "Copy: $file"
  cp $1 $2
}

# Create any-dark imageset
function create_dual_imageset() {
  file=$1
  file_base=${file%.*}
  light_icon=$light_dir/$file
  dark_icon=$dark_dir/$file
  imageset=$assets_dir/$file_base".imageset"
  mkdir -p $imageset

  light_pdf=$file_base"_light.pdf"
  dark_pdf=$file_base"_dark.pdf"
  overwrite $light_icon $imageset/$light_pdf
  overwrite $dark_icon $imageset/$dark_pdf
  cat <<- EOF > $imageset/Contents.json
{
  "images" : [
    {
      "filename" : "$light_pdf",
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "filename" : "$dark_pdf",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  },
  "properties" : {
    "preserves-vector-representation" : true
  }
}
EOF
}

# Create any imageset
function create_any_imageset() {
  file=$1
  file_base=${file%.*}
  icon=$light_dir/$file
  imageset=$assets_dir/$file_base".imageset"
  #rm -rf $imageset
  mkdir -p $imageset

  overwrite $icon $imageset/$file
  cat <<- EOF > $imageset/Contents.json
{
  "images" : [
    {
      "filename" : "$file",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  },
  "properties" : {
    "preserves-vector-representation" : true
  }
}
EOF
}

# Usage
if [ ! -d $light_dir -o ! -d $dark_dir ] ; then
  cat <<- EOF

1. Download all icons by "Download all assets" with "Original" naming convention:
    - https://app.zeplin.io/project/[light]
    - https://app.zeplin.io/project/[dark]
2. Unzip those as:
    - $light_dir -- light icons
    - $dark_dir -- dark icons

EOF
  exit 1
fi

# Diff
ls $light_dir > $tmpdir/light.txt
ls $dark_dir > $tmpdir/dark.txt
if ! diff $tmpdir/light.txt $tmpdir/dark.txt ; then
  echo "There are differences."
  exit 1
fi

# Clean assets
# TODO: create diff and remove
#for file in $assets_dir/*.imageset ; do
#  rm -rf $file
#done

# Import
icons=`cd $light_dir; ls -1`
for file in $icons ; do
  file_base=${file%.*}
  light_icon=$light_dir/$file
  dark_icon=$dark_dir/$file

  if is_identical $light_icon $dark_icon ; then
    create_any_imageset $file
  else
    create_dual_imageset $file
  fi
done
```

# 今後の課題

- 削除されたIconに対応する
- エクスポートまでデザイナーにお願いして、GitHub Actionで取り込むPRを作る

# 最後に

最初はポチポチ登録してたので、一括インポートは感動でした😭
あとimagemagickめちゃ便利、という話でした。
他にもこんな方法もあるよとか、フィードバックもぜひお願いします！
(SketchやFigmaからエクスポートした場合はどうなるんだろう…？🤔)