[iOS] UICollectionViewCompositionalLayout の概要

1.はじめに

UICollectionViewCompositionalLayout とは iOS13 から登場した、 CollectionView 用のレイアウトをより柔軟に定義できる比較的新しいレイアウトクラスです。

ぱっと見、とっつきづらかったのですが、いくつかの考え方を把握すれば、けっこうスルッと理解が進むのではと思います。

と言うわけで、本ポストでは細かいことは置いておいて、概要を把握するために押さえておくべき基本の考え方を書いていこうと思います。

2.前提条件

Mac: macOS Big Sur 11.5.1
Xcode: Version 13.2.1(13C100)

3.UICollectionViewCompositionalLayout とは

まず UICollectionView では基本的には SectionItem の組み合わせによってデータソースを定義していきます。
これは UICollectionViewCompositionalLayout を利用していても変わりません。

また、既存の UICollectionViewFlowLayout も基本的には SectionItem によってレイアウトを決めていっていました。

では、UICollectionViewCompositionalLayout では何が違ってくるのか。
レウアウトの際、Group という概念が一つ加わります。

この Group が重要な概念です。

4.Group とは

GroupSectionItem の間に挟まってくるレイヤーになります。
その名の通り、Item をグルーピングする機能を持つ概念です。

Group は別の Group を内包することができ、入れ子にすることが可能です。

以下のようなイメージとなります。


UICollectionViewCompositionalLayout の基本的な概念

ここで一点意識しておくべきは、Group はあくまでレイアウトを設定するときのみに利用する概念です。

ですので例えば cellForItemAtdidSelectItemAt などはこれまでと変わらず
引数に IndexPath(SectionItem) が渡ってきます。

5.レイアウトをどう定義していくか

これは公式の動画やサンプルを見るのがよいかなと思います!

一番シンプルなレイアウトから順を追って複雑なレイアウトにしていっているので、
とても分かりやすいかと思います。

内容をかいつまんで言うと、以下のような感じかなと思います。

  1. Item たちを Group に追加、Group たちを SectionSection たちを Layout (つまり UICollectionViewCompositionalLayout) に追加していく
  2. 各要素の高さや幅を指定する際、絶対値はもちろん、一つ上のレイヤーを基準に割合指定などもできる
  3. 各要素を縦に並べるか横に並べるかを指定できる
  4. 各要素をいくつ並べるかを指定できる
  5. 他にも色々宣言的にリッチな見た目を作成できる

6.UICollectionViewDataSource との組み合わせ

ここまでは主に Layout に関して書きましたが、
以降は既存のAPIの組み合わせと、どう組み合わせられるかを書いていきます。

これまでの UICollectionView では UICollectionViewDataSource
numberOfSectionsnumberOfItemsInSectioncellForItemAt などで
要素の数や各Cellの見た目などを指定していました。

UICollectionViewCompositionalLayout で定義する場合でも
それらは利用できます 🙌

ここで一つ疑問が。

numberOfItemsInSection で指定した数と
UICollectionViewCompositionalLayout で指定した Item の数が合わない場合、
クラッシュなど発生しないのか。

結論から言うと数が合っていなくてもクラッシュは発生しません。

以下の画像のように、指定したレイアウトに沿って必要な数の要素が表示されていきます。

実際のItem の数によってどのように表示されていくか

何となく、たこ焼き器の鋳型に必要な分だけ生地を流し込むのをイメージしてしまいました 🐙


さて、ここまでデータソースの指定の仕方として
素の UICollectionViewDataSource で書いてきました。

が、より新しい指定方法として UICollectionViewDiffableDataSource があります。

そちらを使うこともできますが(というかAppleの上記サンプルコードはそちらを使っています)、それはまた別ポストでまとめようと思います。

7.UICollectionViewDelegate との組み合わせ

didSelectItemAtdidHighlightItemAt などのデリゲートメソッドは
これまでと同様に利用可能です。

引数には IndexPath が渡ってきます。

8.UICollectionViewDelegateFlowLayout との組み合わせ

sizeForItemAt などがありますが、こちらのデリゲートメソッドは呼ばれません。

異なるレイアウト方法を指定しているので当然か 🤔

9.さいごに

最近?のiOSアプリ開発は SwiftUI や async/await など、
新しい概念がどんどん出てきていると感じています。

そろそろ本格的に実践投入可能な機会も増えてきたかなと思うので
きちんと理解していこうと思います!

[OSS] ゆくゆくはコントリビュートしたいので、どういう心構えで何から始めればよいかの情報に触れてみた

1.はじめに

以下の書籍を読んで感銘を受けたので、
特に心に刺さった箇所や得た知見を、忘れないようにまとめていきます。

※同内容のドキュメントも GitHub にて公開されています。
GitHub: これでできる! はじめてのOSSフィードバックガイド ~ #駆け出しエンジニアと繋がりたい と言ってた私が野生のつよいエンジニアとつながるのに必要だったこと~

OSS にコントリビュートしたいけどどうすればよいの?
という方の背中を押してくれる書籍です。

2.OSS とは

普段何気なくOSS と言っていますが、
Open Source Initiative という団体が「OSS とされるもの」についての条件を記載しており、
それが OSS についての標準的な解釈だと考えられます。

定義の原文はこちらにあります。
Open Source Initiative: The Open Source Definition

Open Source Group Japan という非営利団体が日本語訳を公開しているので、
そちらを参照するのもよさそうです。
OSG-JP: オープンソースとは? その定義とは?


ちなみに、OSS (オープンソースソフトウェア) という言い方は日本独自のようで、
海外では一般的には Open Source と呼ばれるようです。
(Software の部分が余分…)
Shuji Sado: OSSという日本でしか通用しない三文字略語について

3.OSS のライセンス

OSS と言えばライセンスです。

Open Source Initiative はまた、
「OSSであると認められるライセンス」の一覧を公開しています。

Open Source Initiative: Licenses by Name

ここに存在しないライセンスを付与されているソースコードはOSS ではない、
と言っても間違いではないのかなと思います。

とは言え、かの有名な GPL なども列挙されていますし、
リストに記載があったとしてもライセンスの内容は確認した方がよさそうです。

4.OSS に貢献する際の行動規範

Contributor Covenant というサイトにて、
OSS に関わる(= コントリビュートする)際の行動規範が文書化されています。

英語や日本語訳は以下から閲覧できます。
Contributor Covenant: Contributor Covenant Translations

こちらを読んで、コントリビューターはどう振る舞うべきかを
把握しておくとよさそうです。

また、行動規範というとパッと見は堅苦しいものに見えますが、
そこまで深刻なものではないかと思います。

行動の例として以下のようなことが挙げられています(一部抜粋)。

– 他人への共感と優しさを示す
– 異なる意見、視点、経験を尊重する

Contributor Covenant: コントリビューター行動規範 (CC BY 4.0 License)

イチ開発者として誠実で建設的であるならば、
大きな問題はないのかなと思います。

5.最初の取っ掛かり

無数にあるOSS の中からどうすれば貢献の取っ掛かりを見つけられるのか…。

一つの方法として、GitHub で good first issue ラベルが付いた issue から探す、
というのがありそうです。

この good first issue というのは、
「初めてのコントリビュートにちょうどよい issue」に付けられるラベルです。

そのリポジトリでの初心者向けの issue なので、
敷居が低いんじゃないかなと思います。

正直、上記の Github の検索結果から探すのはきついと思いますが、
以下のサイトではプログラミング言語毎にカテゴライズされているので、
まずはそちらから探してみるのもよさそうです 🙌
good first issue

6.OSS に関わる際の心構え

4.OSSに貢献する際の行動規範 の前段の話にはなりますが。

書籍では「イシュートラッカーに関わる時の心構え」として以下の記述がありました。

そこに関わるときは、開発に関わる仲間としての振る舞いが求められることに注意が必要です。

これでできる! はじめてのOSSフィードバックガイド ~ #駆け出しエンジニアと繋がりたい と言ってた私が野生のつよいエンジニアとつながるのに必要だったこと~: イシュートラッカーに「お客さま」として関わって起こる不幸 (著作者: OSS Gate、結城洋志(YUKI “Piro” Hiroshi)) (CC BY-SA 4.0)

issue に初めて関わる場合でも、必要な情報を整理・提供したり、
何が分からないのか、何の情報が足りないのか、などを具体的に記載する。

そういったエンジニアとしての振る舞いが必要ということかと思います。

7.オープンソースだからと言ってタダ乗りに門戸が開かれている訳ではない

6.OSSに関わる際の心構え と似たような内容ではありますが、
以下の文章も僕の心に刺さりました。

開発に関わりたい・品質向上に寄与したいという積極的な意志を持つ人にとっての門戸が開かれている

これでできる! はじめてのOSSフィードバックガイド ~ #駆け出しエンジニアと繋がりたい と言ってた私が野生のつよいエンジニアとつながるのに必要だったこと~: 誰もが開発者、開発関係者として関われる (著作者: OSS Gate、結城洋志(YUKI “Piro” Hiroshi)) (CC BY-SA 4.0)

OSSに関わる以上、積極的な意志や行動を持っていないと
「何しにきたんだ」と思われてしまうということなのかなと思います。

アジャイル動物園における「鶏」になってしまう、というイメージでしょうか 🤔

逆に言うと、OSSに既に関わっているメンバーは
積極的な意志や行動を持っているということなので、
身が引き締まる思いがしました。

8.その他、心に刺さったリンク

書籍では有益なリンクがたくさん貼られていましたが、
特に自分に刺さったリンクを貼ります。

  1. 別にしんどくないブログ: Node.jsへのコントリビュート解説、そしてOSSへ貢献するということ
    1. 本ポストより深く、より具体的に、
      OSSへコントリビュートする際のノウハウがまとめられています。
  2. Speaker Deck: OSSで結果を出す方法
    1. どのような観点からPRを出すべきか、など
      コントリビュートする際に持っておくべき視点についてまとめられています。

コントリビュートする際には、どちらも一読するとよいのではないかと思います。

9.さいごに

僕はあまり本を読む方ではないですが、
この本に出会って運がよかったと思いました。

仕事でOSSを使う際、
「巨大な体制で強いエンジニアばかりだろうから自分には何もできない」
と思っていましたが、その抑制感が少し解消された気がします。

そしてHTMLを触り始めた頃の
「これを駆使すれば何でも好きなように表現できる」感を思い出しました。

僕自身はけっこうフッ重なのですぐにコントリビュートはできなさそうですが、
できるよう徐々に環境などを整えていこうと思っています。

以上です。
お疲れ様でした!

[Mac][Server] Macにローカルサーバを立てる方法

1.はじめに

簡単なWebページ確認などをするため、
サクッとローカルサーバを立てたい時があるかと思います。

そのようなときに使える方法を3つまとめます。

最初の2つはコマンドラインで1秒で簡易サーバを立ち上げる方法、
3つ目はApacheサーバを立ち上げる方法です。

2.前提条件

Mac: macOS Big Sur バージョン11.5.1
Python: 2.7.16 (古い…)
PHP: 7.3.24 (古い…)
Apache/2.4.46 (Unix)

3.コマンド1行で簡易的なサーバを立てる方法

HTML などの静的なファイルを確認するだけなら、コマンド1行で事足ります。

3-1.pythonコマンド

Python2 の場合、以下のコマンドをターミナルで打ちます。
8888 はポート番号です。

python -m SimpleHTTPServer 8888

Python3 の場合は以下のコマンドでできるようです(未検証です 🙇‍♂️)

python -m http.server 8888

すると、コマンドを打ったディレクトリに index.html がある場合、
http://localhost:8888/ でアクセスできるようになります。

もしくは同じネットワーク内なら、異なるデバイスからでも
http://192.168.0.1:8888/ のようにMac のローカルIPアドレス指定で
アクセスすることができます。

また、test.html がある場合はもちろん
http://localhost:8888/test.html でアクセスできるようになります。

参考:
Qiita: Macで簡易ローカルサーバーをたてる
Serverworks: Python の http.server を使って簡単にWebサーバーを立てる

4.phpコマンド

以下のコマンドをターミナルで打ちます。
8888 はポート番号です。

php -S localhost:8888

python のときと同様、コマンドを打ったディレクトリに index.html がある場合、
http://localhost:8888/ でアクセスできるようになります。

ただし、こちらの手法の場合はローカルIPアドレスを指定しても
異なるデバイスからのアクセスはできないようです。

参考: オリジナルゲーム.com: 【Mac】ローカルにWebサーバを立ち上げる超簡単な方法!

4.Apacheサーバを立てる方法

上記2つの方法はとても簡単ですが、スクリプトが動かなかったりするため、
静的ファイル以外の確認ができない場合があります。

そのような場合はApache サーバを立てるのも一つの手かなと思います。

今回は .conf 系のファイルを3種類修正します。

以下のサイトを参考にさせていただきました。

4-1. Sites ディレクトリを作成する

まずは Sites ディレクトリを作成します。

mkdir ~/Sites

4-2. /etc/apache2/httpd.conf を修正する

以下、粛々と修正していきます。

自分一人しか使わないことを想定し、今回は最低限として
こちらの http.conf においては3点のみ修正をします。

まずは157行目付近の mod_http2.so のコメントアウトを外します。
これで HTTP/2 を使えるようになるようです。

LoadModule http2_module libexec/apache2/mod_http2.so

PHPを使いたい場合は 187行目付近の libphp7.so のコメントアウトを外します。

LoadModule php7_module libexec/apache2/libphp7.so

521行目付近の httpd-userdir.conf のコメントアウトを外します。
これで /etc/apache2/extra/httpd-userdir.conf を読み込みます。

Include /private/etc/apache2/extra/httpd-userdir.conf

これで /etc/apache2/httpd.conf の修正は完了です。

4-3. /etc/apache2/extra/httpd-userdir.conf を修正する

上記で読み込み可能になったファイルです。

以下のコメントアウトを外します。

Include /private/etc/apache2/users/*.conf

4-4. /etc/apache2/users/<username>.conf を修正する

上記で読み込み可能になったファイルです。
<username>は自分のアカウント名などに読み替えてください。

以下のように記述します。
8888 はポート番号です。

Listen 8888
<VirtualHost *:8888>
    DocumentRoot "/Users/<username>/Sites"
    <Directory "/Users/<username>/Sites">
        Require all granted
    </Directory>
</VirtualHost>

Apache の設定は以上です。
あとは起動するだけです。

4-5. Apache を起動する

先述しましたが、以下のコマンドを打つとApache が起動します。

sudo apachectl start

すると、コマンドを打ったディレクトリに index.html がある場合、
http://localhost:8888/ でアクセスできるようになります。

もしくは同じネットワーク内なら、異なるデバイスからでも
http://192.168.0.1:8888/ のようにMac のローカルIPアドレス指定で
アクセスすることができます。

今回は libphp7.so も有効にしたので PHP ファイルも実行できます。

4-6. Apache を停止する

以下のコマンドでOKです。

sudo apachectl stop

こうするとブラウザからは http://localhost:8888/ などでアクセスできなくなります。

4-7. 補足1: .conf ファイルなどを編集せずともサーバは立てられるが…

これまで書いたような .conf ファイルの編集を行わずとも
Apache サーバは立てられます。

例えば、何も編集せずに

sudo apachectl start

とコマンドを打ってブラウザから localhost にアクセスすると
It works! と表示されるHTMLが閲覧できます。

ここで参照されるのは /Library/WebServer/Documents/index.html.en となります。
任意のファイルを /Library/WebServer/Documents/ に置いて
同様にブラウザから参照することもできるかもしれません🤔

が、この手法は推奨されていないようです。

その代わりにSites ディレクトリをドキュメントルートにするのがデファクトのようなので
そちらの本ポストではそちらの方法を記載しました。

Mac のあるバージョンまでは
Sites ディレクトリがデフォルトで用意されてたようですが、
最近のバージョンではそうでもないようです。
(Lion あたりで変わったのかな? 🤔)

4-8. 補足2: /etc と /private/etc は同じ

/etc/private/etc といったパスが出てきましたが、
Mac では /etc/private/etcシンボリックリンクとなっています。

$ ls -l /etc
lrwxr-xr-x@ 1 root  wheel  11  1  1  2020 /etc -> private/etc

ですので、例えば以下の2つは同じファイルを指しています。

  • /private/etc/apache2/extra/httpd-userdir.conf
  • /etc/apache2/extra/httpd-userdir.conf

Apache 周りの設定などを調べていると
/etc/private/etc のパスが色々出てきて混乱してしまいますが、
同じファイルを指していると分かれば少しは把握しやすいかもしれません 🙌

5.さいごに

クライアントアプリ開発においてもたまに
このような確認をしたい場面がありますね。

お役に立てればと思います。

以上です。
お疲れ様でした!

[Mac][Zsh] 文字列を置換・整形してきれいなXMLにする

1.承前

前編 のおさらい。

以下のサンプルを My.junit というファイル名で利用しました。

<?xml version='1.0' encoding='UTF-8'?>
<testsuites name='All' tests='5' failures='0' >
  <testsuite name='Tests/AAATests' tests='5' failures='0'>
    <testcase classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>
    <testcase classname='AAATests' name='テストケースB()' time='0.002708911895751953'/>
    <testcase classname='AAATests' name='テストケースC()' time='5.103403783017138107'/>
    <testcase classname='AAATests' name='テストケースD()' time='2.847301841843703481'/>
    <testcase classname='AAATests' name='テストケースE()' time='0.902874301873418053'/>
  </testsuite>
  <testsuite name='Tests/BBBTests' tests='5' failures='0'>
    <testcase classname='BBBTests' name='テストケースA()' time='3.004773974418640137'/>
    <testcase classname='BBBTests' name='テストケースB()' time='0.003311038017272942'/>
    <testcase classname='BBBTests' name='テストケースC()' time='0.003813487107181401'/>
    <testcase classname='BBBTests' name='テストケースD()' time='11.384701784318703814'>
      <failure>Assertion Failure at BBBTests</failure>
    </testcase>
    <testcase classname='BBBTests' name='テストケースE()' time='0.841047132138008907'/>
  </testsuite>
</testsuites>

そして以下のコマンドを実行しました。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5

そうすると、以下のような出力結果を得られました。

11.384701784318703814'> classname='BBBTests' name='テストケースD()'
5.103403783017138107'/> classname='AAATests' name='テストケースC()'
3.004773974418640137'/> classname='BBBTests' name='テストケースA()'
2.847301841843703481'/> classname='AAATests' name='テストケースD()'
0.902874301873418053'/> classname='AAATests' name='テストケースE()'

今回は、その出力結果を更に整形して、人間が見やすいXMLファイルにします!

2.前提条件

  • Mac: macOS Big Sur 11.5.1
  • Zsh: zsh 5.8 (x86_64-apple-darwin20.0)
  • AWK: awk version 20200816
  • Tidy: HTML Tidy for Mac OS X released on 31 October 2006 – Apple Inc. build 1045

3.最終的なコマンド

前編にも載せました が再掲します。

$ echo \<testsuites\> \<testsuite\> `cat YourProject.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's@$@/>@'` \<\/testsuite\> \<\/testsuites\> | tidy --input-xml true -utf8 --wrap 0 --indent true --add-xml-decl true -o output.xml

以降、出力結果を整形していきます!

4. XMLの閉じタグの残骸を除去する

出力結果(再掲) を見ると、>/> など XMLの閉じタグの残骸があります。

11.384701784318703814'> classname='BBBTests' name='テストケースD()'
5.103403783017138107'/> classname='AAATests' name='テストケースC()'
3.004773974418640137'/> classname='BBBTests' name='テストケースA()'
2.847301841843703481'/> classname='AAATests' name='テストケースD()'
0.902874301873418053'/> classname='AAATests' name='テストケースE()'

まずはこれを除去していきます。
閉じタグの残骸を空文字へと置換します。

前編で sed を使って文字列置換をしていきました が、今回も同様にします。

sed による置換は以下のようになります。

sed -e 's/置換前の文字列/置換後の文字列/'

> の置換は以下コマンドになります。

sed -e 's/>//'

となると、/> の置換は以下コマンドになると思いきやそうではありません。

sed -e 's//>//'

上記例だと sed のスクリプトは / で区切りを指定しているため、
置換対象の文字列に / が入っているとスクリプトが正しく解釈されません。

/ をそのままの文字で扱うにはエスケープという手法があります。
そのままの文字として扱いたい(つまりエスケープしたい)文字の前に
バックスラッシュ \ を付けます。

今回の例で言うと以下のようなコマンドになります。

sed -e 's/\/>//'

これでも正しいのですが、個人的には / が多くて読みづらいと感じてしまいます。
(これに関しては慣れの問題なのかも知れません 🤔)

そこで、区切り文字として / 以外の文字を指定します。
そうすることで、/ を区切り文字としてではなく、そのままの文字として扱えます。

sed は区切り文字に \ と改行文字以外は使用できるようです。

Any character other than backslash or newline can be used instead of a slash to delimit the RE and the replacement.

man sed の [2addr]s/regular expression/replacement/flags

/ の次のデファクトは把握していないですが、本ポストでは @ を使うことにします。

というわけで、/> を空白文字にするためには以下のコマンドを使います。

sed -e 's@/>@@'

上記2つの置換を使って、XML の閉じタグの残骸を除去します!

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@'

sed -e ‘s/>//’ と書ける部分の区切り文字に関しても @ で書きました。
また、sed コマンドは繋げて記述できるのでそうしています。

すると以下のような出力になり、XML の閉じタグが除去できました!

11.384701784318703814' classname='BBBTests' name='テストケースD()'
5.103403783017138107' classname='AAATests' name='テストケースC()'
3.004773974418640137' classname='BBBTests' name='テストケースA()'
...略

5.各行をXMLのタグで囲む

最終的にはXMLのフォーマットにしたいので、まずは各行をXMLのタグで囲んでいきます。

ここでも sed を使います。
各行頭と各行末、それぞれに文字を追加していきます。

5-1.各行頭に文字列を追加する

まずは行頭に <testcase time=' という文字列を追加します。
そのためのコマンドは例えば以下になります。

sed -e "s/^/<testcase time='/"

ここでポイントとなるのが、「行頭」の指定方法です。
sed では、と言うよりも正規表現の仕様では、行頭を ^ で表現できます。
参考: The Open Group Library: Regular Expressions

また置換後の文字列にシングルクォートを利用するので、
スクリプトの前後をダブルクォートで囲っています。

ですので行頭に文字列を追加する場合は上記のようになります。

というわけで、全体としては以下のコマンドになります。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/"

すると、以下のように出力されます。

<testcase time='11.384701784318703814' classname='BBBTests' name='テストケースD()'
<testcase time='5.103403783017138107' classname='AAATests' name='テストケースC()'
<testcase time='3.004773974418640137' classname='BBBTests' name='テストケースA()'
...略

5-2.各行末に文字列を追加する

次に行末に /> という文字列を追加します。
そのためのコマンドは以下になります。

sed -e 's@$@/>@'

上にも書いた通り、区切り文字に / を使ってしまうと
/ そのものの文字列にはエスケープ処理が必要となるので、
簡単のため区切り文字は @ にしてあります。

ここでのポイントは「行末」の指定方法です。

正規表現にて、行頭を表すのに ^ というメタ文字がありました。
それと同様に、行末にも専用のメタ文字があり、それが $ となります。
参考: The Open Group Library: Regular Expressions

そして、全体のコマンドは以下のようになります。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's@$@/>@'

すると、以下のような出力になります。

<testcase time='11.384701784318703814' classname='BBBTests' name='テストケースD()'/>
<testcase time='5.103403783017138107' classname='AAATests' name='テストケースC()'/>
<testcase time='3.004773974418640137' classname='BBBTests' name='テストケースA()'/>
...略

だいぶ XML ぽくなりました 🙌

6.全体をXMLのタグで囲む

直前の出力だとルート要素がないので正式なXMLドキュメントではありません。

ですので、テキスト全体の先頭と末尾にタグを追加して、
ルート要素を作っていきたいと思います。

今回は echo を使います。

前回のポストの「最終的なコマンド」でも記載しましたが、
echo を使わずにこの処理も sed でできそうでした。
そちらの方がスマートかもと思いましたが、今回は echo を使います。

例えば以下のコマンドを打つと

echo aaa

以下のように出力されます。

aaa

また、例えば以下のコマンドを打つと

date

以下のように出力されます。

2022年 2月14日 月曜日 00時00分00秒 JST

では、以下のようなコマンドを打つとどうなるかと言うと。

echo date

以下のように出力されます。

date

date がコマンドではなく、文字列として解釈されてしまいます。
echo の際に date をコマンドとして扱うにはどうすればよいか。

バッククォート \で囲います。
以下のようなコマンドにすると。

echo `date`

以下のように出力されます。

2022年 2月14日 月曜日 00時00分00秒 JST

つまり、echo の際 バッククォート で囲むと、
その中身をコマンドとして実行し、その結果を出力する という挙動になります。

これを使うと、以下のようなコマンドを打った場合、

echo AAA `date` BBB

以下のように出力されます。

AAA 2022年 2月14日 月曜日 00時00分00秒 JST BBB

で、今回はJUnit 風の XML にしたいと考えたので、このような文字列を出力したいです。
<testsuites><testsuite><testcaseたち /></testsuite></teststuites>

それをコマンドにすると以下のようになります。

echo \<testsuites\> \<testsuite\> `これまでのコマンド` \<\/testsuite\> \<\/testsuites\>

<> はコマンドのリダイレクトに使用されるため、
バックスラッシュでエスケープしています。

ということで、バッククォートの中身をこれまでのコマンドに置き換えると
以下のようになります。

$ echo \<testsuites\> \<testsuite\> `cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's@$@/>@'` \<\/testsuite\> \<\/testsuites\>

すると以下のように出力されます。

<testsuites> <testsuite> <testcase time='11.384701784318703814' classname='BBBTests' name='テストケースD()'/> <testcase time='5.103403783017138107' classname='AAATests' name='テストケースC()'/> <testcase time='3.004773974418640137' classname='BBBTests' name='テストケースA()'/> <testcase time='2.847301841843703481' classname='AAATests' name='テストケースD()'/> <testcase time='0.902874301873418053' classname='AAATests' name='テストケースE()'/> </testsuite> </testsuites>

読みづらいですが、XMLのフォーマットになりました!🙌

あとはこれを人間が読みやすいように改行とインデントを加えます。

7.Tidy を使って人間が読みやすい形にXMLを整形する

Mac には xmllint が入っていますが、マルチバイトを認識しない?ようなので、
同じく Mac に入っている Tidy を使います。

Tidy とは HTML ファイルを検証、修正、きれいに出力するコマンドです。
(man tidy より)

Git リポジトリもあります。
また各コマンドのリファレンスもあります。

HTML 用のコマンドですがXMLも扱えます。

さっそく使っていきます。
以下のコマンドでXMLとして扱いつつ、文字コードをUTF-8 に指定します。

$ echo \<testsuites\> \<testsuite\> `cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's/$/\/>/g'` \<\/testsuite\> \<\/testsuites\> | tidy --input-xml true -utf8

すると以下のように出力されます。

<testsuites>
<testsuite>
<testcase time='11.384701784318703814' classname='BBBTests'
name='テストケースD()' />
<testcase time='5.103403783017138107' classname='AAATests'
name='テストケースC()' />
<testcase time='3.004773974418640137' classname='BBBTests'
name='テストケースA()' />
<testcase time='2.847301841843703481' classname='AAATests'
name='テストケースD()' />
<testcase time='0.902874301873418053' classname='AAATests'
name='テストケースE()' />
</testsuite>
</testsuites>

大体良さそうですが、微妙なところに改行があるのが気になりますね。

これは Tidyのワードラッピングの処理が入っているためで、
デフォルトでは68文字が上限となっているようです。

改行は不要にしたいので、--wrap 0 を追加します。

$ echo \<testsuites\> \<testsuite\> `cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's/$/\/>/g'` \<\/testsuite\> \<\/testsuites\> | tidy --input-xml true -utf8 --wrap 0

以下のように表示されます。

<testsuites>
<testsuite>
<testcase time='11.384701784318703814' classname='BBBTests' name='テストケースD()' />
<testcase time='5.103403783017138107' classname='AAATests' name='テストケースC()' />
<testcase time='3.004773974418640137' classname='BBBTests' name='テストケースA()' />
<testcase time='2.847301841843703481' classname='AAATests' name='テストケースD()' />
<testcase time='0.902874301873418053' classname='AAATests' name='テストケースE()' />
</testsuite>
</testsuites>

いいところまで来ました!
インデントも欲しいので --indent true も追加します。

$ echo \<testsuites\> \<testsuite\> `cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's/$/\/>/g'` \<\/testsuite\> \<\/testsuites\> | tidy --input-xml true -utf8 --wrap 0 --indent true

すると、以下のように表示されます。

<testsuites>
  <testsuite>
    <testcase time='11.384701784318703814' classname='BBBTests' name='テストケースD()' />
    <testcase time='5.103403783017138107' classname='AAATests' name='テストケースC()' />
    <testcase time='3.004773974418640137' classname='BBBTests' name='テストケースA()' />
    <testcase time='2.847301841843703481' classname='AAATests' name='テストケースD()' />
    <testcase time='0.902874301873418053' classname='AAATests' name='テストケースE()' />
  </testsuite>
</testsuites>

これで完璧ですね!🙌

更に --add-xml-decl true で XML宣言を付与しつつ、
-o output.xml で出力先のファイルを指定します。

$ echo \<testsuites\> \<testsuite\> `cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's/$/\/>/g'` \<\/testsuite\> \<\/testsuites\> | tidy --input-xml true -utf8 --wrap 0 --indent true --add-xml-decl true -o output.xml

cat output.xml を実行すると以下のように表示されます。

<?xml version="1.0"?>
<testsuites>
  <testsuite>
    <testcase time='11.384701784318703814' classname='BBBTests' name='テストケースD()' />
    <testcase time='5.103403783017138107' classname='AAATests' name='テストケースC()' />
    <testcase time='3.004773974418640137' classname='BBBTests' name='テストケースA()' />
    <testcase time='2.847301841843703481' classname='AAATests' name='テストケースD()' />
    <testcase time='0.902874301873418053' classname='AAATests' name='テストケースE()' />
  </testsuite>
</testsuites>

これで人間にも読みやすい形でファイル出力できました!🙌

これで完成とします!👏


ちなみに Tidy を実行するたびに No warnings or errors were found. などのメッセージが出ます。
表示したくない場合は -q オプションを付けると、表示されなくなります。

8.さいごに

今回は主に sed と Tidy の使い方をまとめました。

前編 も合わせてけっこうな量になりましたが、
慣れると とても簡単なことをやっていると感じられるかと思います。

お疲れ様でした!
よいUNIXライフを!

[Mac][Zsh] JUnit 形式の XML ファイルから実行に時間のかかっているテストケーストップ5を抽出する

1.はじめに

CIでテスト実行にかかる時間の割合がとても大きく、
どんなテストケースが遅いのか気になったのでシェルで抽出してみました。

長くなってしまったので前後編に分けます。

本ポスト(前編)では、本タイトルの要旨、つまり
「JUnit の実行結果から実行時間の遅いテストケーストップ5を抽出するまで」
を書きます。

そこで抽出した文字列は十分伝わりますが不恰好なので
どうせなら XMLとして整形しちゃおうということで、
後編ではそのあたりを書こうと思います。

2.前提条件

  • Mac: macOS Big Sur 11.5.1
  • Zsh: zsh 5.8 (x86_64-apple-darwin20.0)
  • AWK: awk version 20200816

3.最終的なコマンド

前後編合わせた最終的なコマンドです。

僕が試した JUnit ファイルだと、
以下のzshコマンドを実行して JUnit の XML ライクなフォーマットで出力できました。
(シェル芸人ではないのでスマートさはないと思います)

$ echo \<testsuites\> \<testsuite\> `cat YourProject.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's@$@/>@'` \<\/testsuite\> \<\/testsuites\> | tidy --input-xml true -utf8 --wrap 0 --indent true --add-xml-decl true -o output.xml

ちょっと分かりづらすぎるので、2つに分割すると以下のコマンドになります。

$ echo \<testsuites\> \<testsuite\> `cat YourProject.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s/^/<testcase time='/" -e 's@$@/>@'` \<\/testsuite\> \<\/testsuites\> > wip.xml

$ tidy --input-xml true -utf8 --wrap 0 --indent true --add-xml-decl true -o output.xml wip.xml

1つ目のコマンドで、テスト実行時間の降順でソートし、遅い順に5個ピックアップし、
その結果を wip.xml という名前で保存しています。

これだけでも問題なく xml ファイルになっているんですが、
改行やインデントがなくて可読性が悪い状態になっています。

ですので、2つ目のコマンドで改行やインデントをきれいにし、
output.xml という名前で出力しています。


ちなみに冒頭のワンライナー、echo の処理も sed で代用できるけど、
こちらの方がスマートかも知れない? 🤔

$  cat YourProject.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5 | sed -e 's@/>@@' -e 's@>@@' -e "s@^@<testcase time=\'@g" -e 's@$@/>@g' -e '1s@^@<testsuites><testsuite>@' -e '$s@$@</testsuite></testsuites>@'| tidy --input-xml true -utf8 --wrap 0 --indent true --add-xml-decl true -o output.xml

4.JUnit 実行結果の XML の形式について

今回僕が試したのは以下のようなフォーマットでした。

<?xml version='1.0' encoding='UTF-8'?>
<testsuites name='All' tests='4' failures='0' >
  <testsuite name='Tests/AAATests' tests='2' failures='0'>
    <testcase classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>
    <testcase classname='AAATests' name='テストケースB()' time='0.002708911895751953'/>
  </testsuite>
  <testsuite name='Tests/BBBTests' tests='2' failures='0'>
    <testcase classname='BBBTests' name='テストケースA()' time='0.004773974418640137'/>
    <testcase classname='BBBTests' name='テストケースB()' time='0.003311038017272949'/>
  </testsuite>
</testsuites>

検索したところ、JUnit の公式のフォーマットはなさそうという記述が多かったので、
各環境によって実行結果のフォーマットは微妙に異なってくるのかなと思います。

ただ、GitHub には JUnit のリポジトリ があり、JUnit5 のフォーマットが定義されています
ですので JUnit5 に関してはこれが公式なのかなと思います。


これ以降、冒頭に書いたコマンドについて順を追って説明します!

5.例として使用する XML ファイル

以下のサンプルを My.junit というファイル名で利用します。

<?xml version='1.0' encoding='UTF-8'?>
<testsuites name='All' tests='5' failures='0' >
  <testsuite name='Tests/AAATests' tests='5' failures='0'>
    <testcase classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>
    <testcase classname='AAATests' name='テストケースB()' time='0.002708911895751953'/>
    <testcase classname='AAATests' name='テストケースC()' time='5.103403783017138107'/>
    <testcase classname='AAATests' name='テストケースD()' time='2.847301841843703481'/>
    <testcase classname='AAATests' name='テストケースE()' time='0.902874301873418053'/>
  </testsuite>
  <testsuite name='Tests/BBBTests' tests='5' failures='0'>
    <testcase classname='BBBTests' name='テストケースA()' time='3.004773974418640137'/>
    <testcase classname='BBBTests' name='テストケースB()' time='0.003311038017272942'/>
    <testcase classname='BBBTests' name='テストケースC()' time='0.003813487107181401'/>
    <testcase classname='BBBTests' name='テストケースD()' time='11.384701784318703814'>
      <failure>Assertion Failure at BBBTests</failure>
    </testcase>
    <testcase classname='BBBTests' name='テストケースE()' time='0.841047132138008907'/>
  </testsuite>
</testsuites>

6.testcase という文字列のある行のみを抜き出す

以下のコマンドを打ちます。

$ cat My.junit | grep 'testcase.*'

ターミナルに以下のように出力されます。

    <testcase classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>
    <testcase classname='AAATests' name='テストケースB()' time='0.002708911895751953'/>
    <testcase classname='AAATests' name='テストケースC()' time='5.103403783017138107'/>
    <testcase classname='AAATests' name='テストケースD()' time='2.847301841843703481'/>
    <testcase classname='AAATests' name='テストケースE()' time='0.902874301873418053'/>
    <testcase classname='BBBTests' name='テストケースA()' time='3.004773974418640137'/>
    <testcase classname='BBBTests' name='テストケースB()' time='0.003311038017272942'/>
    <testcase classname='BBBTests' name='テストケースC()' time='0.003813487107181401'/>
    <testcase classname='BBBTests' name='テストケースD()' time='11.384701784318703814'>
    <testcase classname='BBBTests' name='テストケースE()' time='0.841047132138008907'/>

7.実行時間の遅い順にソートする

いよいよここから本題です。

実行時間でソートするため、まずは秒数を各行の文頭に持ってきます。

その前準備として、各 testcase 要素内の属性を並べ替えます。

いまは classname, name, time の順番になっていますが、
これをtime, classname, name の順番にします。

ここでは AWK を使って並べ替えることにします。

7-1.AWK とは

AWK とは、基本的には

  1. 文頭から、指定したレコード分離文字(デフォルトは改行コード)までが現れるまでを1レコードとする
  2. 各レコード毎に、指定したフィールド分離文字(デフォルトはスペース)でレコード内の文字列を分割し、指定したアクションを行う

ということをするプログラミング言語です。
と言いつつ、機能拡張により最近のプログラミング言語のように複雑なこともできるようです。


AWKに関しては丁寧な解説ページがたくさんありますが、
僕は主に以下のサイトを参考にさせていただきました。

  1. Qiita: awkコマンドの基本
  2. hydroculのメモ: awk コマンド
  3. とほほのWWW入門: とほほのAWK入門

7-2. AWK を使ってtime=を先頭に持ってくる

ソートをするため、

<testcase classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>

のような行にて、time 属性の 0.008349895477294922 を文頭に持ってきたいです。
前準備として time=’0.008349895477294922′ を文頭に持ってこようと思います。

そのためには半角スペースを区切り文字とし、
分割された文字列の塊たちを並べ替えて、time= を先頭に持ってこればよさそうです。

まずは以下のようなコマンドを打ちます。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print $1, $2, $3, $4}'

すると、以下のように出力されます。

<testcase classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>
<testcase classname='AAATests' name='テストケースB()' time='0.002708911895751953'/>
<testcase classname='AAATests' name='テストケースC()' time='5.103403783017138107'/>
... 略

あまり変わったようには見えないです🤔

ちなみにここの awk では、「フィールド分離文字は半角スペースである」と
-F ' ' オプションで指定しています。
デフォルトなのでこのオプションは省略しても挙動は変わりません。

次に以下のコマンドを打ちます。
上のコマンドと比べると、print の引数から $1 を消しています。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print $2, $3, $4}'

すると、以下のような表示になりました。

classname='AAATests' name='テストケースA()' time='0.008349895477294922'/>
classname='AAATests' name='テストケースB()' time='0.002708911895751953'/>
classname='AAATests' name='テストケースC()' time='5.103403783017138107'/>
... 略

各行から <testcase という文字列が消えてますね!

つまり、元の各行は半角スペースで分割されており、
$0 には <testcase という文字列が入っていることになります。

$4 には time='0.008...'/> という文字列が入っているので、$4 を先頭に持ってきます。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3}' 

すると、以下のように表示されます。

time='0.008349895477294922'/> classname='AAATests' name='テストケースA()'
time='0.002708911895751953'/> classname='AAATests' name='テストケースB()'
time='5.103403783017138107'/> classname='AAATests' name='テストケースC()'
... 略

簡単に文字列操作できました 🙌

次に、文頭の time=' という文字列を除去します。
そのために sed コマンドを使います。

7-3. sed とは

sed とは、基本的には

  1. ある文字列に対して、命令に沿って文字列の追加や削除などを行う
  2. 命令は「1行目に指定した文字列を追加しろ」などが使えるが、最も使われるのは正規表現による文字列置換
  3. awk は各行内での文字列の抽出が得意だが、sed は文全体での文字列の編集が得意

のような感じかと思います。

sed に関しても丁寧な解説ページがたくさんありますが、
僕は主に以下のサイトを参考にさせていただきました。

  1. TECHNOSCORE BLOG: あえていうほどでもないsed入門
  2. hydroculのメモ: sed コマンド
  3. Qiita: sedでこういう時はどう書く?

7-4. sed を使って行頭のtime=’ を除去する

ソートをするため、

time='0.008349895477294922'/> classname='AAATests' name='テストケースA()'

のような文字列から time=' を除去します。
time=' を空文字列に置換することにします。

sed で正規表現を用いて置換する場合は

sed -e 's/置換前の文字列/置換後の文字列/'

というコマンドになります。
スラッシュ 「/」で区切っていくのが一般的です。

ですので、今回は time=' という文字列を空文字に置換したいので、
何も考えずに当てはめると以下のようなコマンドになります。

sed -e 's/time='//'

ですがこれはダメです。
実行すると以下のような表示になると思います(環境によって違うかも知れません)。

pipe pipe pipe quote>

なぜかと言うと、スクリプト、つまり s/time='// の記述を
シングルクォートで開始しているため、
time=' でスクリプトの末尾だと解釈されてしまうからだと思われます。

それを回避するために、スクリプトの前後をダブルクォートで囲みます。
(シングルクォートのエスケープ方法もあるかも知れませんが、今回は追求しません)

sed -e "s/time='//"

ということで、以下のコマンドを打ちます。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//"

すると、以下のように表示されます。

0.008349895477294922'/> classname='AAATests' name='テストケースA()'
0.002708911895751953'/> classname='AAATests' name='テストケースB()'
5.103403783017138107'/> classname='AAATests' name='テストケースC()'
...略

簡単に文字列操作できました 🙌
あとはソートして上位何件かを取得するだけです。

まずはソートします!

7-5. sort を使ってソートする

@IT: 【 sort 】コマンド――テキストファイルを行単位で並べ替える を参考にしました。
Linux の記事ですが、今回の用途的には違いはありませんでした。

降順でソートするので、r オプションを使えばよさそうです。

以下のコマンドを打ちます。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -r 

すると、以下のような表示になります。

5.103403783017138107'/> classname='AAATests' name='テストケースC()'
3.004773974418640137'/> classname='BBBTests' name='テストケースA()'
2.847301841843703481'/> classname='AAATests' name='テストケースD()'
11.384701784318703814'> classname='BBBTests' name='テストケースD()'
0.902874301873418053'/> classname='AAATests' name='テストケースE()'
...略

何かがおかしいです。

ソートはされていますが、実行時間の遅い順ではありません。
各行の1文字目だけに着目すればソートはされていると言えますが、
どうも数値として扱われていないようです。

文字列を数値として扱うよう n オプションを追加してみます。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn

すると以下のような表示になります。

11.384701784318703814'> classname='BBBTests' name='テストケースD()'
5.103403783017138107'/> classname='AAATests' name='テストケースC()'
3.004773974418640137'/> classname='BBBTests' name='テストケースA()'
...略

実行時間順にソートすることができました!🙌

トップ5を抽出してみます。

7-6. head を使って先頭N行を抽出する

これは簡単。

以下のコマンドで実現できます。

head -数値

というわけで、以下のコマンドを実行します。

$ cat My.junit | grep 'testcase.*' | awk -F ' ' '{print$4, $2, $3 }' | sed -e "s/time=\'//" | sort -rn | head -5

先頭5行、つまり実行時間の遅い順に上位5件が抽出できたかと思います!

11.384701784318703814'> classname='BBBTests' name='テストケースD()'
5.103403783017138107'/> classname='AAATests' name='テストケースC()'
3.004773974418640137'/> classname='BBBTests' name='テストケースA()'
2.847301841843703481'/> classname='AAATests' name='テストケースD()'
0.902874301873418053'/> classname='AAATests' name='テストケースE()'

文字列の整形はされていませんが、テキストで共有する場合はこれで十分伝わりますね!

これで最低限やりたいことはできました!

長くなったので一旦ここで切って、本ポストは後編に続けます。

後編は趣向が変わって、
抽出したデータを XML 文書として扱えるよう文字列を追加・削除する、
XML のインデントなどを整形をするという内容になります。

8.さいごに

sed や awk は難しそうなイメージがあったんですが、
やってみると思ったよりとっつきやすかったです。

数万行程度のテキストなら秒で処理できますし、とてもパワフルですね!

勉強して損はなさそうです!

後編に続きます。
お疲れ様でした!

[Android] MaterialCardView を使って画像を角丸表示する

1.はじめに

Android で画像を角丸化したいのですが、
何か属性をいじればサクッとできるのかと思いきやちょっと手間がかかりました!
(iOSならサクッとできるのに!)

ということで本ポストでは MaterialCardView を使って画像を角丸化する手法をまとめます。
参照: Android Developers: MaterialCardView

2.前提条件

Mac: macOS Big Sur 11.5.1
Android Studio: Arctic Fox | 2020.3.1 Patch 3
Gradle: 7.0.2

3.事前準備: まず素の画像を表示する

レイアウトXML に以下のように記述します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/sample_image" />

</androidx.constraintlayout.widget.ConstraintLayout>

シンプルに200×200 の画像を画面中央に配置するレイアウトです。

エミュレータで見るとこんな感じです。

画面キャプチャ

ちなみに画像は以下を「sample_image」というリソース名でプロジェクトに追加しています。
(必要なら自由に使ってください)

sample_image

画像の追加方法がうろ覚えな方は、例えば本ブログの
Web-y.dev: [Android] プロジェクト作成からViewBinding を使って画像表示するまでをまとめる
を参考にしていただければと思います。

4.角丸化する

いよいよ角丸化します。

冒頭で言った通り、MaterialCardView を使って丸くします。

レイアウトXMLを以下のように編集します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.card.MaterialCardView
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:cardCornerRadius="40dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            app:srcCompat="@drawable/sample_image" />
    </com.google.android.material.card.MaterialCardView>

</androidx.constraintlayout.widget.ConstraintLayout>

ImageView に一枚 MaterialCardView のレイヤーを被せてやるイメージです。
そうすると以下のように画像が角丸になりました!

角丸化された

一点注意点として、アプリのテーマが MaterialComponents かその子テーマでない場合、
エラーが発生することがあるようです。

その場合は以下のように
MaterialComponents などのテーマを指定すれば回避できるかと思います。

android:theme=”@style/Theme.MaterialComponents.Light”

参考: Stack Overflow: https://stackoverflow.com/a/61244323

また、MterialCardViewFrameLayout を継承しています。

そのため、画像の親要素が FrameLayout だった場合は、
基本的にはそれを MaterialCardView に変更するだけで角丸化に対応できるかと思います。

5.さいごに

iOS だと

View.layer.cornerRadius

のように一行噛ませるだけで角丸化できたので、
それよりは手間がかかりますね😇

以上です。
お疲れ様でした!

[iOS][Gemfile] bundle instsall すると Gemfile.lock に差分が出てしまう時の対処法

1.はじめに

複数人で開発をしていると、依存ライブラリのバージョンを縛る場合が多いと思います。
例えば Podfile.lock や Cartfile.resolved などでバージョンを縛り、
それに沿って環境構築をすることになります。

Gemfile.lock も同様なのですが、bundle install で環境構築をすると、
自分だけなぜか Gemfile.lock ファイルが更新されてしまう場合がありました。

今回はそれの解決方法についてまとめます。

2.前提条件

Mac: macOS Big Sur 11.5.1
Bundler: とあるバージョン

3.原因

Bundler のバージョンが他の人と異なっている場合、上記のようなことが起こり得ます。
(他の原因もあるかも知れませんが 🤔)

4.Bundler とは

Bundler provides a consistent environment for Ruby projects by tracking and installing the exact gems and versions that are needed.

https://bundler.io/

訳: Bundlerは、必要なgem やバージョンを正確に追跡してインストールすることで、
Rubyプロジェクトに一貫した環境を提供します。

4-1.Gem とは

The RubyGems software allows you to easily download, install, and use ruby software packages on your system. The software package is called a “gem” which contains a packaged Ruby application or library.

https://guides.rubygems.org/

訳: RubyGems は、Rubyのソフトウェアパッケージを簡単に
ダウンロード、インストールして、システム上で使用することができるソフトウェアです。
ソフトウェアパッケージは「gem」と呼ばれ、
Rubyのアプリケーションやライブラリがパッケージ化されています。


つまり Bundler とは Ruby 用ライブラリ(つまり gem) の
パッケージやそのバージョンを指定・特定してインストールすることができる gem

ということになります。

パッケージやそのバージョンの指定は Gemfile に記述します。
そして bundle installbundle update をすると、
gem ライブラリの依存関係が Gemfile.lock に出力されます。

5.解決方法

タイトルの件について、例えば以下のような方法があるかなと思います。

  1. bundle install 時のバージョンを指定する
  2. Bundler のバージョンを合わせる
  3. Gem のバージョンを合わせる

上に行くほど暫定的な対応になります。

逆に言うと下に行くほど根本的な対応になりますが、
その分影響範囲が広くなります。

5-1.bundle install 時のバージョンを一時的に合わせる

これは個人の環境のみで完結できます。

他の人の Bundler のバージョンを聞きます。

bundler -v

で、他の人の Bundler のバージョンがが例えば 2.0.2 なら、
以下のようなコマンドを打てばよいです。

bundle _2.0.2_ install

バージョン番号の前後にアンスコを付ける形です。

2.0.2 が自分のマシンに入っていない場合は、
まず以下のようにインストールする必要があります。

gem install bundler -v 2.0.2

このようにすると既存の Gemfile.lock を変更することなく環境構築ができるかと思います。

ただし毎回バージョン指定して bundle install する必要がありそうです。
やや面倒くさいですね 🤧

5-2.Bundler のバージョンを合わせる

みんなで Bundler のバージョンを合わせます。

他の人のマシンに Bundler の特定のバージョンが入っていない場合は、
上記と同様にインストールしてもらう必要があります。

gem install bundler -v 2.0.2

この手段の場合、他の人も bundle install をすると
Gemfile.lock に差分が出る状態となる場合があります。

ですので今後はその新しい Gemfile.lock を共通で使っていくことになります。

5-2-1.他の人は差分が出ない場合

自分のBundler のバージョンが他の人の Bundler よりも進んでいる可能性があります。
その場合は自分のBundler のバージョンを下げます。

以下のようにして自分のバージョンのBundler を消しましょう。

gem uninstall bundler -v 2.1.4

もし以下のようなワーニングが表示されて消せない場合。

Gem bundler-2.1.4 cannot be uninstalled because it is a default gem

自分がGem のデフォルトバージョンのBundler を使用していて、
それは消せない、と怒られてしまっています。

そのような場合は、後述のようにみんなでGem のバージョンを合わせるか、
Gem のデフォルトバージョンのBundler を削除するか、
の選択肢があります。

Gem のデフォルトバージョンのBundler を削除する方法は以下に詳しいです。
Qiita: docker内でGem(特にbundler)のdefault設定を解除したい!

が、この方法はあまりおすすめできないようです。
他のGem ライブラリで、
デフォルトバージョンを指定したBundler を利用している可能性があるためです。

rbenv を使っている場合、デフォルトはこの辺りに定義されています。
(パス中のバージョンは適宜読み替えてください)

/Users/<username>/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/specifications/default/

5-2-2.なぜ他の人は差分が出ないのか

自分のBundler のバージョンが他の人より進んでいて 2.1.4 だった場合。
他の人が bundle install をすると、2.1.4 未満となる訳で、
少なくとも以下のBundler のバージョン記述に差分が出て欲しいものです。

BUNDLED WITH
   2.1.4

が、ここの箇所には差分は出ません。

これはBundler の仕様として、
BUNDLED WITH よりも古いバージョンの bundler を使っている人は、
bundle install しても BUNDLED WITHの記述を更新しない、ということのようです。

この辺りのことは以下に詳しいです。
Qiita BUNDLED WITH で Gemfile.lock が更新されてしまう件

5-3.Gem のバージョンを合わせる

みんなで Gem のバージョンを合わせる方法です。
これでデフォルトのBundler のバージョンも一致すると思います。

gem update --system

この辺りは以下の記事に詳しいです。
Qiita: 【Rails】bundlerのdefaultを変更する方法。 Warning: the running version of Bundler (x.x.x) is older than the version that created the lockfileの対処法。

6.最後に

複数人での開発をしていると共通の環境構築をするのは難しいですね 😣

以上です。
お疲れ様でした!

[iOS][Bundler] bundle install で Errno::EACCES: Permission denied @ dir_s_mkdir … というエラーが出た時の対処法

1.はじめに

bundle install とコマンドを打つと以下のようなエラーが出る時があります。

Errno::EACCES: Permission denied @ dir_s_mkdir - /Users/<username>/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/extensions/x86_64-darwin-20/2.7.0/json-2.6.1
An error occurred while installing json (2.6.1), and Bundler cannot continue.
Make sure that `gem install json -v '2.6.1' --source 'https://rubygems.org/'` succeeds before bundling.

それの解消法を記載します。

2.前提条件

Mac: macOS Big Sur 11.5.1
Bundler: 2.0.2

3.原因

ディレクトリが root ユーザになっていることが原因の可能性があります。

僕の場合は
/Users/<username>/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/extensions/x86_64-darwin-20/
以下が root ユーザでした。

$ ls -al /Users/<username>/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/extensions/2.7.0/extensions/x86_64-darwin-20/
drwxr-xr-x  3 root     staff   96  9 22 17:19 .

そのため、コマンド実行時に権限がないと怒られていたということです。

4.解決方法

以下のコマンドでユーザを再帰的に変更すれば解決できるかと思います!

chown -R <username> /Users/<username>/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/extensions/x86_64-darwin-20

<username> の部分は自身のユーザ名で置き換えてください。

5.最後に

少しでも助けになれば幸いです。

以上です。
お疲れ様でした!

[Android] プロジェクト作成からViewBinding を使って画像表示するまでをまとめる

1.はじめに

本ポストでは以下の内容に馴染むことを目的としています。

  1. 画像をプロジェクトに組み込む(単一ファイルをdrawable に入れる)
  2. ViewBinding を導入する
  3. ViewBinding を使って動的にViewを操作する

参考

  1. Android Developers: Resource Manager でアプリの UI リソースを管理する
  2. Android Developers: ビューバインディング

2.前提条件

Mac: macOS Big Sur 11.5.1
Android Studio: Arctic Fox | 2020.3.1 Patch 3
Gradle: 7.0.2

3.プロジェクトを作る

まずは Empty Activity からプロジェクトを作成します。

Empty Activity を選択してプロジェクト作成

4.画像をプロジェクトに組み込む

最低限の手順ということで、何も考えずに一つの画像を組み込みます。

画像の組み込みは Resource Manager にて行います。
以下のようにAndroid Studio の左端から Resource Manager を選択します。

Resource Manager

左端にない場合には、以下のようにツールバーから
View -> Tool Windows -> Resource Manager で辿れます。

Resource Manager

Resource Manager を開いたら + ボタンを押し、Import Drawables を選択します。

Import Drawables を選択

インポートする画像を選択し、Open を押します。
すると以下のような画面が表示されるので、ファイル名を入力して右下の Next を押します。
今回は適当に sample_image と指定しています。

ファイル名を入力してNextを押す

確認画面が表示されるので Import ボタンを押します。
すると画像が追加されました!

Resource Manager に画像が追加されている

Project で確認すると、drawable 配下に追加されているのが分かります。

drawable 配下に追加されている

これで Android Studio 内で画像リソースを扱えるようになりました。

5.ViewBinding を導入する

build.gradle (Module) を開きます。

build.gradle (Module) を開く

android ブロック の中に、以下のように buildFeatures を記述します。

plugins {
    ...
}

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    ...
}

その後、Android Studio の右上にある「象さんアイコン」を押して、
プロジェクトをシンクします。

象さんアイコンを押す

これで ViewBinding が利用可能になりました!🙌

6.ViewBinding を使って動的にViewを操作する

いよいよ ViewBinding を使っていきます。
本ポストでは上記でインポートした画像を ViewBindiing で動的にセットしていきます。

6-1.空の ImageView を画面に配置

その前に下準備として、空の ImageView を画面に配置します!
activity_main.xml を開いて、以下のようにレイアウトを記述します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>

デザイン的には以下のようになります。

レイアウト

「imageView」というIDを指定した画像を画面中央に配置しています。
ViewBinding から操作する際はこのIDを元に画像を操作することになります。

表示されているアバター画像は tools 属性指定のため、開発時にのみ表示される画像です。
参照: Android Developers: ツール属性のリファレンス

これで準備が完了しました!
あとはコードで動的に画像のリソースをセットします。

6-2.ViewBinding を使う

MainActivity.kt を開き、MainActivityClass を以下のように記述します。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)

        binding.imageView.setImageResource(R.drawable.sample_image)

        val view = binding.root
        setContentView(view)
    }
}

ActivityonCreate() メソッドに処理を記述します。

この記述についてもう少し詳細に見ていきます。


以下の記述で、ActivityMainBinding のインスタンスを生成しています。

binding = ActivityMainBinding.inflate(layoutInflater)

スタティックな inflate() メソッドで生成されるんですね。
ちなみに ActivityMainBinding クラス自体は自動で生成されています。

そして以下の記述で、ActivityMainBinding を介して
「imageView」というIDを持つ ImageView に、画像をセットしています。

binding.imageView.setImageResource(R.drawable.sample_image)

inflate() メソッドがどういうメソッドなのか最初はイメージが湧きづらいかもしれませんが、
バインディングクラスのインスタンスを生成するメソッドということで、
一つ覚えておくべきポイントかなと思います。

生成したバインディングクラスを介してビューを操作していくという形ですね!

ちなみに以下のコードでバインディングクラスの root を参照していますが、
これはビュー内の要素ではなくビューのルートを取得できるプロパティになっています。

val view = binding.root

今回の例でいうと、activity_main.xml に記述した
androidx.constraintlayout.widget.ConstraintLayout を取得しています。


最後にアプリを実行しておきます。

7.アプリを実行する

エミュレータのキャプチャ

狙い通りコードでセットした画像が表示されました!🙌

8.最後に

本文中にも書きましたが、
inflate() メソッドでバイディングクラスのインスタンスを生成し
そのクラスを介してビューを操作する、
と覚えておけば ViewBinding の利用方法をイメージしやすいのかなと思います。

以上です。
お疲れ様でした!

[Android] ビルド時に Use ‘–warning-mode all’ … というワーニングが出た場合の確認方法

1.はじめに

Android Studio でアプリをビルドする際、以下のようなワーニングが出ました。

Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/7.0.2/userguide/command_line_interface.html#sec:command_line_warnings

Gradle で非推奨の機能を使っているので、
個々の内容を見るときは --warning-mode all を利用してください、
と言われています。

本ポストではこれの設定方法を書きます。
5秒で設定できます。

2.前提条件

Mac: macOS Big Sur 11.5.1
Android Studio: Arctic Fox | 2020.3.1 Patch 3
Gradle: 7.0.2

3.設定する

gradle.properties を開きます。

gradle.properties を開く

そのファイルに以下を追記します。

org.gradle.warning.mode=all

その後、Android Studio の右上にある「象さんアイコン」を押して、
プロジェクトをシンクします。

象さんアイコンを押す

そうすると、次のビルドログの冒頭でより詳細なワーニングを確認できます!


ちなみに設定できる値は all, fail, summary, none の4つがあります。
参照: Gradleの公式ドキュメント

4.最後に

Gradle は iOS で言うところのプロジェクトファイルのようなものですね 🤔

以上です。
お疲れ様でした!