【インターンレポート】 Flex Messageの改善

はじめに

はじめまして。LINE夏インターンシップ技術職就業型コースに参加していた、髙﨑 環(たかさき めぐる)と申します。普段は大学院で自然言語処理についての研究をしています。過去にはWebアプリやiOSアプリの開発にも取り組んだこともあります。

今回のインターンでは、LINEコミュニケーションプラットフォーム開発室にある「LINEコミュニケーションサービス開発チーム」にて6週間、iOSエンジニアとしてアプリ開発に携わりました。この記事では、私の取り組んできた題材の一つである「Flex Message」について開発内容をご紹介したいと思います。

Flex Message

Flex Messageとは

LINE DevelopersのFlex Messageのページでは、次のような説明がなされています。

Flex Messageとは、レイアウトをカスタマイズできるメッセージです。CSS Flexible Box(CSS Flexbox)(opens new window)の基礎知識を使って、レイアウトを自由にカスタマイズできます。
FlexコンテナがFlex Messageのボックスに対応しています。また、FlexアイテムがFlex Messageのコンポーネントに対応しています。

「あっ、これのことか!」と思った方もいらっしゃるのではないでしょうか。LINE NEWSなどのLINE公式アカウントからのメッセージとして使用されることも多いため、トーク履歴を遡ってみると見つかるかもしれません。Flex Messageはテキストや画像だけのメッセージと比べて、複雑なレイアウトで情報を発信・共有することができるというメリットがあります。Flex MessageはJSON形式のデータで記述でき、このデータをMessaging APIを経由して送信することで、Coolにレイアウトされたメッセージがサービス使用者に届けられます。

本ブログでは、私が開発に携わった内容のうち、「maxSize・lineSpacing機能の追加」、「Tall Script表示の改善」、「ダークモード向けプロトタイプ実装」を代表して取り上げ、ご紹介させていただきます。具体的な内容紹介に入る前に、まずはFlex Message全体に関わる技術背景について少しだけ説明をしたいと思います。

技術背景: SnapshotTesting

Flex Messageはコミュニケーションアプリである「LINE」で用いられる機能ではありますが、開発効率を向上させるためにサイズの小さいデモアプリを用いて開発を進めています(ビルドが早くて嬉しい!)。デモアプリはトーク画面を模したシンプルな作りとなっており、JSON形式で記載されたテストデータが表示されています。

Flex Messageのテストにおいて確認する必要があるのは「レイアウト」です。機能追加により意図したレイアウトが実現できているか、変更した機能が期せずして別のレイアウトに作用していないか、などを細かく検証する必要があります。開発を進めるにあたってテストデータは増えていきますし、それぞれのレイアウトについて1pxのずれが発生しているかを目視で判断するのは…非常にイライラする作業でしょうし、そもそも正確だとはいえなさそうですよね…

そこで、Flex Messageのデモアプリでは、SnapshotTestingを用いたテスト方法が導入されています。この方法は、それぞれのテストデータについて、現在のコードで描画されたレイアウトと、過去に記録された正しい描画のレイアウト間で、差異の有無を自動で判定するというものです。そのため、新たな機能を実装したりバグを修正したりする場合には、該当部分のコーディングに加え、

  1. ShapshotTestingで既に登録されているデータに差異が発生していないことを確認
  2. 追加機能を検証するテストデータを追加
  3. 追加したテストデータのSnapshotを記録することで、テストケースを更新

することが必要となります。

技術背景: Yoga

Flex Messageでは、Yogaというライブラリを用いて各コンポーネントを実装しています。これは、React Native の内部で使用されているレイアウトエンジンで、iOS版・Android版双方のFlex Messageで採用されています。FlexMessageが開発される前は、HTMLデータからWebViewを用いてレイアウトを描画するTemplate Messageが開発・使用されていました。しかし、WebViewを用いた描画のパフォーマンスが安定しない問題や、新機能を追加する際の工程数の多さを考慮し、Yogaを用いたFlex Messageの開発が進められてきたそうです。Yogaを用いたレイアウトでは、CSS Flexboxのようにレイアウトを実装できるため、画面サイズや要素数の違いに対しても、少ない記述量で安定した描画をすることができます。次に述べる「maxSize・lineSpacing機能の追加」についても、JSONからのParserの変更、Yogaを用いて実装されたNodeへの値の設定、テストケースの追加という少ない工程で実装することができました。

技術背景の説明はここまでで、いよいよこのブログの本題に入りましょう。次の章からは、私が実際に行った開発内容を順にご紹介していきます。

maxSize・lineSpacing機能の追加

Flex Messageは現在も定期的に更新が行われていますが、次のアップデートでは指定可能になるパラメータが新たに追加される予定です。(現在までに指定可能なパラメータと、コンポーネントの詳細はこちら)今回私は、このアップデートに関する開発の一部に携わりました。私が携わったものは「maxSize」と「lineSpacing」の2つです。

maxSize

maxSizeは、Box Componentに新たに追加したプロパティです。Boxとは、メッセージのレイアウトを定義するためのコンポーネントで、Box、Button、imageなどの他のコンポーネントを含めることができます。このBoxのwidth(幅)とheight(高さ)を明示的に指定することは今までも可能でしたが、今回の開発ではmaxWidth(最大幅)とmaxHeight(最大高さ)の2つのプロパティを追加し、最大サイズを明示的に指定できるようにしました。下に添付している左側の画像は、maxSize用のテストケース画像です。黄色のBoxは、maxWidthとmaxHeightをwidthとheightより小さい値に設定しているため、maxSizeを設定していない水色Boxに対して、大きさが制限されていることがわかります。

lineSpacing

lineSpacingは、Text Componentに新たに追加したプロパティです。Textとは、その名の通り文字列を描画するためのコンポーネントです。デフォルトでは1行のみの表示で、サイズを超えると文字列がカットされるのですが、wrapプロパティにtrueを指定することで、自動で折り返して複数行表示することが可能になります。今回、Textが複数行表示される時の行間サイズを明示的に指定するために、lineSpacingプロパティを新たに追加することになりました。下に添付している右側の画像は、lineSpacing用のテストケース画像です。水色のBox内のTextではlineSpacingを3px、ピンクでは10pxに設定していて、行間が正しく反映されていることがわかります。

Feature Flagを用いた開発

機能追加に関わる一連のアップデートでは、Feature Flagを用いた開発が採用されていました。

Feature Flagは、プロダクト全体での有効/無効を指定するbool値のことです。今回の場合は、対象アップデートのリリース前か後かを管理するためのFeature Flagを追加しました。このFlagのTrue/Falseを判定する条件文中に、maxSizeやlineSpacingといった新しい実装を記述することで、アップデートに関わる動作をFlagによってまとめて制御しています。

Feature Flagを用いた開発のメリットとして、「開発途中の機能を製品に組み込むことができる」「同条件での制御を一律で行いやすく、管理がしやすい」などが挙げられます。ここで、Feature Flagを使用した機能開発の対となるFeature Branchという開発プロセスを取り上げ、詳しく説明します。

Feature Branchを用いた開発方法では、アップデート内容をまとめるBranchを用意し、開発によって生じた変更を逐次Feature Branchに反映させていき、全ての変更が完成後にまとめてMain Branchにマージするというプロセスを取ります。Feature Branchを用いた開発は一般的なのですが、変更内容が膨大な場合や開発に関わる人数が多い場合には、最終的にMain Branchとマージする時にコンフリクトが複雑に発生することが危惧されます。一方で、Feature Flagを個々の機能について追加し、それぞれの変更をFeature Branchにマージするのではなく、直接Main Branchに反映する開発方法を取ると、一度に発生するコンフリクトの量を抑えることが可能なほか、トラブル発生時に変更を戻す場合にも細かい単位で行うことができるというわけです。

Tall Script表示の改善 

Tall Scriptとは

Tall Scriptという概念を皆さんはご存知でしょうか。ここからは少しだけiOS開発からは離れ、各言語の文字・フォントについて少しだけ触れたいと思います。

Tall Scriptとは、その名の通り「高さのある活字」で、一般的な文字に比べて縦長の文字についてこのように表現されます。その一例として、タイ語があげられます。タイ語で用いられる文字には、子音・母音・末子音(単語の終端で用いられる子音)・声調符号(音の高低を示す符号)の4種類があります。そして、子音の上下左右に母音が、さらに上部分に声調記号が付与されるように表現されます。

下図では二つのタイ文字の構成を紹介しています。ロボットが合体するように子音に各文字が追加されていき、最終的には日本語や英語の場合と比べて大きな一文字であるかのように表現されるのです。Tall Scriptはタイ語以外にも、デヴァーナガリーを用いるヒンディー語やネパール語などで確認できます。(参考:https://devstreaming-cdn.apple.com/videos/wwdc/2016/201h1g4asm31ti2l9n1/201/201_internationalization_best_practices.pdf

ちなみに、フォントや文字列の違いは「文字列の高さ」以外の観点もあると思います。例えば、日本語や中国語のように縦書きが可能な言語もあれば、アラビア語のように右から左に書き進める言語もあります。単語間でスペースを開ける言語ではスペースで改行することを意識する必要がありますし、特定の言語に対応するために選んだフォントだとレイアウトが崩れる、という可能性もあります。今回取り上げるタイ文字に限らず、多言語へのUIの対応は往々にして発生しうることを意識して、開発をする必要があります。

UIFont in Swift

本題に入る前に、Flex Messageで用いられているUIFontのパラメータについて整理しておきましょう。Apple Developerのドキュメントにわかりやすい図があったので引用させていただきます。

Flex Messageで使用しているフォントは、どの言語でも共通でSan Franciscoでした。このフォントではLine gapは0で、Line height = Ascent + Descentが成り立ちます。

問題点: TextNodeにおけるバグ

ここからが本題です。Flex Messageに報告されているTall Script表示のバグの例を図にしました。


図のように、タイ文字の一部において上端の声調記号が切れてしまったり、声調記号が上の文字のDescent部分と重なってしまうバグが発生することがわかりました。原因として考えられるのは、使用フォントのサイズと実際の描画範囲のミスマッチです。タイ文字の子音の上に母音と声調記号が付属した場合に、フォントで指定されているAscentないしLine heightを超えた部分で描画が発生してしまうため、フォントにデフォルトで設定されているパラメータを元にTextNodeを設計すると、文字が切れてしまうケースが発生すると考えられます。

今回取り上げるTextNodeでは、文字列の詳細な装飾をNSAttributedStringを用いることで実装しており、

  1. parseしたプロパティを元にNSAttributedStringを設定する(例: color, text, lineSpacing…)
  2. NSAttributedStringをlabel.attributedTextに設定する

というプロセスでUIlabelにテキストデータを描画しています。そのため、仮にテキストが一行の場合、

label.clipsToBounds = false

に設定することで、ラベルの領域であるlabel.boundsからはみ出したテキストも描画されるため、この問題は解決します。しかし、今回のケースのように複数行にまたがる場合だと、一行目と最終行目はバグが解消されるものの、それ以外の行についてはclipsToBoundsの修正ではバグが解消されませんでした。このバグについて、「paddingを上側に追加することで解決できるのでは」という議論がなされていましたが、同様の理由で中間行についてはバグが依然残ってしまいます。

また、WWDC2016の資料P108-115で紹介されているように、Dynamic Typeを用いて言語に合わせた適切な行間隔のフォントを設定し、複数行の場合にも対処するという方法も考えられました。具体的には、

label.font = UIFont.preferredFont(forTextStyle: .body)

のように、Dynamic Typeを反映したTextStyleをフォントの設定時に指定する方法です。しかし、フォントサイズの指定などのFlex Messageの機能が損なわれてしまうことや、デバイスで設定されたフォントサイズがDynamic Typeを反映したフォントにのみ反映されることから、この方針での実装は難しいという判断をしました。

改善方法

そこで、シンプルなアプローチとして、Line height自体を明示的に設定しました。Line heightとは、「UIFont in Swift」で先述したとおり、フォントにおいてテキストの高さを表すパラメータなのですが、タイ文字の場合には上側(ascent)で指定された範囲を超えた部分に描画されます。これが複数行となった場合、タイ文字の上側が描画されない現象や、上の行の文字と重なる現象が生じると推測されます。これは行間を広く確保するか、行の高さを高く設定することで対策できると考えられますが、今回は後者の方針を採用しました。前者を選ばなかった理由としては、

  1. lineSpacingが未実装の段階だったため(ブログでは読みやすさのために順番を前後しています)
  2. lineSpacingの設定だけでなくpaddingTopを加える必要があったため

です。タイ文字での声調記号の描画に必要な高さについては、AscentとCap heightの差程度と仮定をおきました。そして、NSAttributedStringが複数行になった際の挙動を設定する.paragraphStyleを次のように設定しました。(該当範囲のみ抜粋)

let paragraph = NSMutableParagraphStyle()
paragraph.minimumLineHeight = font.lineHeight + (font.ascender - font.capHeight)
attributes[.paragraphStyle] = paragraph

この場合の問題点として、他の言語にも適用されてしまうというデメリットがあります。Dynamic Typeを用いた実装などとは違い、この設定が日本語や英語などのTall Scriptを含まない文字列にも適用されてしまいます。今回は、レイアウト全体が崩れてしまうことを避けるために、正規表現でタイ文字が含まれているかを判定し、その場合のみ上記のlineHeightを適用するようにしました。改善後のレイアウトは下図の通りで、上端が切れる問題や上の行と重なる問題は解消されています。

ダークモード向けプロトタイプ実装

ダークモードとは

ダークモードとは、周囲が暗くても画面が見やすくなるように、描画される色を調整する機能です。iPhoneの設定からダークモードを選択すると、ホーム画面の色が黒を基調としたものに変化することがわかると思います。iOSアプリ開発でもこのダークモードに対応する必要があり、LINEアプリでは既にダークモードが実装されています。

問題点

Flex Messageがダークモードに対応していないことが現状の問題点として挙げられます。先述の通りLINEアプリ自体はダークモードに対応しており、次の画像のテキストメッセージのを見ると、灰色ベースの吹き出しに白い文字で描画されていることがわかります。しかし、Flex Messageで送信している部分(「Brown Cafe」と題したメッセージ)は白背景に黒字のままで、色の変換は行われません。

Flex Messageでのダークモード実装において、主に二つの課題が挙げられます。一つ目は、ユーザーがダークモードを明示的に指定するか否かを検討する必要があることです。先述した通り、Textの色やBoxの背景色を指定する時に、現在は一種類のみを指定しています。ダークモード対応を考える場合、ユーザがダークカラーを明示的に設定するように実装するのか、暗示的に色を変換するのか、もしくは双方のハイブリット案を採用するのかを検討する必要があります。二つ目は、任意の色の設定に対応しなくてはいけないことです。現在の実装では、色の指定方法は「青」などの選択肢があるわけではなく、任意のカラーコードです。そのため、色ごとの変換表を作成するのは現実的ではなく、どの色が設定された場合でもライトモードからダークモードへ変換する関数を実装する必要があります。

改善方法

この問題に対しては、インターンの時間的制約や他開発との兼ね合いもあり、改善方法の提案とプロトタイプの実装まで行いました。以下に実装案とその内容を述べます。

1. ダークカラープロパティの追加

まず、ダークモードプロパティについてです。検討の結果、ダークカラーをユーザが明示的に指定することを可能にした方が良いと考えました。理由としては、ユーザ側がレイアウトを調整できないと、ダークカラーへの自動変換で意に反したレイアウトになった場合に修正する方法がないためです。自動変換のみでの実装は確かに「ユーザが指定するパラメータが減る」という利点がありますが、ダークカラーがイメージと違う時にはライトカラーから見直す必要があり、非直感的な試行錯誤を強いることになるでしょう。また、ライトカラーとダークカラーを統一したいケースは数多くあると予想されます(ロゴマークや企業のイメージカラーなど)。この場合は、対象のコンポーネントについて、ダークカラー・ライトカラーを同色で設定することで解決できます。

しかし、全てのコンポーネントについてダークカラーを指定するというのは、やはり手間がかかります。そこで、ダークカラープロパティに値が入力されなかった場合は、ライトカラーからダークカラーへ自動変換する実装を提案します。例えば、面積を占める割合が大きい背景やテキストの色は、ダークモードに適した色に設定をする場合が多いでしょう。しかし、ユーザに全てのダークカラー設定を一任するというのは、1. 設定するコンポーネントの多さ 2. 適切な色を選ぶ難しさ の2つの観点から避けるべきだと判断しました。ユーザが自動変換で直したい部分だけダークカラーを設定する方が、手間も少なく直感的だと判断しました。

実際の実装では、ライトカラー・ダークカラー(指定無し可能)を引数に取り、それらを統合させたUIColorを返り値とする関数を設計しました。具体的な処理としては次のようなものになります。

  1. ダークカラーが指定されていない場合は「2.ダークモードカラーを求める方法」に従ってダークカラーを算出する
  2. デバイスの設定がダークモードの場合はダークカラー、ライトモードの場合はライトカラーを返すクロージャをUIColorに実装する
2. ダークカラーを求める方法

先ほど述べた通り、ダークカラープロパティに値が指定されなかった場合はダークカラーを算出する必要があります。ライトカラーからダークカラーへの色の変換と言われると、全ての色を反転させればいいのでは?と考えられそうですが、これは不適切だと考えています。LINEアプリでのダークモード対応は、使用箇所ごとにライトモードとダークモードの色を指定する形で実現していますが、グレー系の色は反転させる一方で、それ以外の色は変換しないからです。この方針に従うと、グレー系の色か否かの判定と、グレー系の場合は色の変換に分けて実装を検討する必要があります。

まず、グレー系の色か否かの判定です。まず、UIColor.getRed関数を用いて、ライトモードで使用される予定の色のRGB値とalpha値(透過度)を取得します。先述した通り、Flex Messageでは任意の色を設定できます。そのため、「RGB値上はグレーではないが極めてグレーに近い色」が設定可能になります。例えば、グレー系統の色は正確に(R,G,B) = (120, 120, 120)のように各RGB値が一致するのですが、ユーザー側はグレーを想定していても、(R,G,B) = (120, 121, 120)のように入力のRGB値がズレている可能性があります。このような場合でもグレー系統の色だと認識するために、RGB値の各要素同士の差が閾値より小さかった場合のみ、グレーとして判定しました。

let threshold: CGFloat = 20
if abs(r - g) * 255 > threshold || abs(g - b) * 255 > threshold || abs(b - r) * 255 > threshold {
    // グレー以外と判断する
}
else{
    // グレーと判断する
}

今回はデモにあるテストデータでレイアウト崩れが発生しないか検証し、閾値を20に設定しています。これは応急処置的な実装であるため、閾値については今後議論するべき内容です。

そして、グレー系の場合についてはRGB値から輝度を求め、輝度を反転させたものをRGB値として設定し直すことで、色の反転を行いました。RGBから輝度を求める式は次の通りです。(ここでのrgb値は0から1のCGFloatで、輝度luminanceも0から1の値をとります。)

let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b

グレー色と仮定して反転を行うには、輝度を反転させたものをRGB値として設定することで可能ですが、今回は単純に1.0からluminanceを引くのではなく、offset値を定めて加算しました。これは、LINEアプリのライトモード/ダークモード下でのメッセージ背景色と合わせるための処理で、他のグレー系の色とのコントラストに大きな影響が出ないよう、統一して変換後の輝度にoffset値を加える処理を行なっています。min関数で1.0以上の値にならないように調節後、グレースケールでダークモード変換後の色を出力しています。

let offset: CGFloat = 56.0/255.0
let invLuminance = min(1.0, 1.0 - luminance + offset)
let darkColor = UIColor(white: invLuminance, alpha: alpha)
3. 実際のレイアウト

上記の方針で実装した画像が次の通りです。上側が自動変換によるダークカラー変換、下側がユーザーが定めたダークカラーへの変換をした画像です。実装した範囲は、BoxNode・TextNode・SpanNode・ContainerStyle (header, hero, body, footer)と、色を扱うほとんどのコンポーネントです。

下に添付した二枚の画像は、ダークモード対応した二つのメッセージの画像で、それぞれライトモード・ダークモードに設定した時の画面です。それぞれの画像の上側のメッセージでは、ユーザ側でダークカラーを設定していませんが、先述したような方針で自動変換が可能となっています。また、下側のメッセージは、一部コンポーネントにユーザーがダークモード用の色を設定した場合のレイアウトで描画されています。具体的には、メッセージのbodyとfooterの背景色を黒に、テキストの一部を白に設定し直しています。左画像では上下のメッセージに見かけ上の違いはありませんが、右画像ではダークカラーの設定が正しく反映されていることがわかります。LINEアプリのメッセージよりもレイアウトをくっきりさせたい!という場合にも、ダークカラーを設定させればこのようにカスタマイズ可能、ということです。

これから開発に必要な検討事項

最後に、これから詳細に実装/検討するべき部分についても触れたいと思います。

まず、SnapshotTestingのダークモード対応です。今回は、あくまでもダークモードの指定と色の変換についての実装にとどめていますが、将来的にはTest部分もダークモードに対応する必要があります。勿論今回の実装に伴って追加されたテストデータだけでなく、過去に追加されたテストデータについても検証しなければならないと考えています。これについては、テスト用スクリプトの書き換えと場合に応じたデータの差し替えが検討の対象となるでしょう。

また、より厳密なダークモードの色の調整も必要でしょう。今回の実装ではLINEアプリとは独立に開発をしました。勿論参考にした部分はありましたが、配色などが完全一致しているわけではありません。LINEアプリのデザインとの親和性を高めるのであればより繊細な調整を行う必要があるでしょう。

インターンを通じての感想

開発について

  • 本インターンでは、LINEやFlex Messageといった、普段から自分が特に使用しているアプリケーションが題材であったため、日常で何気なく使用している機能についてより一層知ることができ、ビルド毎にワクワクするような楽しさがありました。また、やはり目に見えやすい部分での開発はその過程でも楽しく感じることが多く、その点でも魅力的だなと感じました。
  • この記事で書いた以外の開発にも、LINEアプリのUserDefaultsの標準化やシミュレータデバッグの改善、デモアプリのUIの改善などにも携わることができました。どの題材についても学ぶことが多く、やりがいがあったと思います。
  • また、基本的な開発フローやコードの書き方などを確認する機会が多かったことも非常に良かったです。個人的には一年ぶりのiOS開発だったため、キャッチアップが出来るか少し不安な部分もありました。しかし、Code Readabilityの映像講座や、会社に蓄積されているドキュメントなど、困った時に確認するための材料が豊富であったため、自発的に知識の見直しを行いやすく非常に助かりました。Code Reviewやディスカッションを通じて学ぶ知見も多くあり、一つ一つが実りのある経験になったと考えています。また、週一回のiOS Study Sessionでは、LINEで働くiOSエンジニアの方々による技術紹介があり、こちらについても大変刺激的でした。
  • 「規模の大きいアプリケーションをどのように開発していくのか」という観点でも学ぶところが多々ありました。私のような知識の浅いインターン生がチームにjoinしても、混乱を招かないようなルール作りや、コミュニケーションを取りやすいチーム作りがなされていて、安心して開発できる環境が整っていました。適切なコミュニケーションによって不明瞭な点をなくし、パフォーマンスを最大化しようという考え方は、どの仕事をしていく上でも大切な観点の一つになりそうです。将来自分が社会人として生きていく上で教訓としたいと思いました。

インターンでの生活について

  • 昨今の流行病の影響もあり、基本的にリモートで開発を進めていました。オフラインでのコミュニケーションの機会が少ない中でも、少人数でのcoffee break的な集まりや歓迎会など、同じチームの方々と顔を合わせてコミュニケーションを取る機会を積極的に設定してくださり、非常にありがたかったです。特にメンターのChenさんには大変お世話になりました。ほぼ毎日私の方からzoomでお呼び出しして、進捗報告と疑問の解消だけでなく、今後の進路の相談や世間話などまでさせていただきました。インターンという時間制約のある中で規模の大きいプロジェクトの仕様を理解するためにも、相談しやすい環境が設けられていたことは仕事のパフォーマンスを向上させる観点でも助かりました。そして何より、人間関係的にも温かみを感じるチームだったので、非常に嬉しかったです。
  • 大学院での研究・学業との両立についても非常に理解があり、研究室ミーティングなどに伴う有給の取得や一時的な離脱なども、事前に相談すれば問題なく可能でした。何から何まで、ありがとうございました。
  • 家が近いこともあり、度々四谷オフィスに出社しました。こちらは今年移転したこともあり、非常に綺麗で働きやすい環境が整えられていました。特に満足度が高かったのは食事面です。朝は無料朝食として作りたてのおにぎりやパンを食し、勤務中は自動販売機で無料のコーヒーやお茶をゴクゴク飲み、昼は健康的で美味しいお弁当やランチをガツガツ食べていました。本当はいただいた社食を一つ一つ紹介したいところなのですが、それではグルメブログになってしまいそうなので… 代わりに幾つか写真を共有して、このブログの締めくくりとさせていただきます。