おすすめの食べ方を見ながら食材を買える体験を作った話

はじめに

こんにちは、買物事業部のデザイナー兼エンジニアの長野です。

生鮮食品ECサービス「クックパッドマート」の開発チームで、注文ユーザー向けのサービス開発全般を担当しています。

今日は、先日クックパッドマートのiOSアプリでリリースした新機能とその開発プロセスについて、お話ししたいと思います。

クックパッドマートの詳細については、以前にも サービス立ち上げ期の話 や、エンジニアメンバーの連載記事 が投稿されているので、そちらもぜひご参照ください。

食べ方を想像しながら食材を選ぶ

今回リリースした新機能は、クックパッドマートで扱う様々な商品(食材)に対して、その食材を使ったおすすめの「食べ方」を提案する、というものです。

これまでのバージョンのアプリでも、商品におすすめのレシピを紐付けて見せるということは行なっていたのですが、今回のアップデートではより幅広い種類の「食べ方」とそのレシピを見ることができるようになりました。

f:id:yoshiko-nagano:20190627094151p:plain
「食べ方」が表示されるホーム画面と商品詳細画面

例えるなら、お店の人と対面で買い物をするときに「この魚は煮付けでもいいし、塩焼きでも美味しいよ」などと会話して、自分の気分で食卓をイメージして食材を購入する体験のようなものです。

このような機能のかたちに至るまでのプロセスを順を追ってご紹介していきます。

数ある課題の何から手をつけていくか?

カスタマージャーニーマップで現状を把握する

今回プロジェクトの開始時点では、アプリのどの部分に手を入れるか、どのような課題にアプローチするか、何も決まっていませんでした。「色々課題はありそうだけど、何から手をつけよう?」という状態です。そこで、まずは現状を正しく把握するためのカスタマージャーニーマップを作成するところからプロジェクトを開始しました。

f:id:yoshiko-nagano:20190627094202p:plain
作成したカスタマージャーニーマップ

社内でサービスを日常的に使っている数名にインタビューをし、サービスの利用フローとその時の思考の流れをマップに書き起こしました。

本来はよりリアリティのある社外ユーザーの情報を集めたいところですが、まだ開始間もないサービスでユーザー数が少ないこと、社内でも普段使いしている人が複数いたことから、スピード優先で社内リサーチを選択しました。

数名のインタビュー結果から、ユーザー属性によって傾向が見られたので、最終的に3人のペルソナとしてまとめることができました。

非同期型ワークショップで課題を洗い出す

カスタマージャーニーマップからは様々な課題が見えてきます。それらを洗い出すために、チームメンバー全員参加の非同期型ワークショップを行いました。

クックパッドマートのチームは、アプリ開発をするメンバーもいれば、日々の配送を回していく流通系のメンバーなど、担当領域が多岐に渡ります。それぞれの視点から見た課題意識をきちんと洗い出すために、今回は「全員参加」の形式にこだわりました。

とはいえ、総勢25名近いメンバー全員の時間を確保するのは非常にコストが高いです。そこで「非同期」で参加できるワークショップという形をとりました。

ワークショップのやり方は以下です。

  • 開催期間は3日間
  • 3人のペルソナのマップを、席近くの壁3箇所に貼り出す
  • メンバーは好きな時間に席近くのマップを眺めて、気づいた課題と解決策のアイデアをポストイットに書いて貼る
  • 毎日マップの掲載場所を入れ替え、全てのマップに目を通してもらう

f:id:yoshiko-nagano:20190121112748j:plain
非同期ワークショップ会場の様子

結果、3日後には25人の視点から洗い出された現状の課題とアイデアを集めることができました。

課題をマッピングし、優先順位をつける

大量に洗い出された課題は似た観点の課題をグルーピングし、以下の3つに仕分けをしました。

  • 今集中して掘り下げるべきもの
  • 今意識しなくても必然的に取り組むことになるもの
  • 今のフェーズではやらなくてよいもの

f:id:yoshiko-nagano:20190627094514p:plain
グループごとに3色の付箋を貼って仕分けた様子

ここまでくると、現状のサービスの課題とそれらの優先度が自ずと見えてきて、次のトライを集中して考えられる状態になりました。

どうやって課題を解決する?

デザインスプリントで解決策を探る

上記の課題の整理から、直近フォーカスして掘り下げる課題が以下に定まりました。

  • どうすれば、日常の買い物の選択肢になれるか?
  • どうすれば、食材の購入だけに止まらない買い物体験を作れるか?
  • どうすれば、使い続けても飽きないサービスになれるか?

そこで、これらに対する解決策を考えていくためのデザインスプリントを実施しました。

デザインスプリントでは、課題に対して一つのソリューションをプロトタイピングし、ユーザーインタビューを行なって検証するという一連のプロセスを、短期間で集中して行います。(こちらの書籍に詳しく書かれているGoogleの手法を踏襲して実践しています)

f:id:yoshiko-nagano:20190627094601p:plain
スプリント中の様子

f:id:yoshiko-nagano:20190627094627p:plain
家にある食材をベースに食べ方を提案するプロトタイプを作成

ここで作成したプロトタイプがそのまま使えるものになったわけではありませんでしたが、スプリントを通した検証結果から下記のような学びを得ることができました。

  • 食材から単一のレシピへ誘導すると、ユーザーの気分や制約条件(家族の状況、調理時間、使用する調味料や調理器具など)とのミスマッチが起こりやすい (現行アプリはこの状態だった
  • レシピより一段階抽象的な「食べ方(唐揚げ、和え物、煮付けなど)」を複数提示されると、ユーザーは気分に合う料理をイメージしやすい
  • 一つの「食べ方」に対してレシピを複数提示できると、ユーザーは自分の制約条件をクリアしたレシピに出会いやすい

したがって、これらをサービスに落とし込めれば、 いつサービスを訪れても自分にあった料理と必要な食材を見つけることができ、クックパッドマートが日々の買い物の選択肢になれるのではないか という仮説を立てることができました。

本当に実現可能なの?

コアになる技術要素を検証する

スプリントを終えて、試す価値のある仮説が得られたものの『本当にユーザーにとってグッとくる「食べ方」を複数提案できるのか?』というサービスのコアとなる部分の実現可能性には疑問が残っていました。実現できなければプロダクトが成り立たなくなってしまうので、早々に検証を進めました。

幸い、クックパッドには毎日料理をしているユーザーさんが提供してくれるたくさんのデータがあります。これまでのレシピサービスの開発の過程で、レシピデータや検索ログを元にして、食材に対する「食べ方」のデータを返す機能が複数開発されていました。そのような既存機能を活用して、クックパッドマートに適した「食べ方」を提案する方法がないかを検証していきました。

検証の対象としたのは、レシピサービスの検索部分でもすでに利用されている「食べ方検索」機能のロジックや、検索キーワードと関連性の強い食材を抽出するロジック、食材に対して多くのユーザーがよく作っている定番のメニューを抽出するロジックなどです。

f:id:yoshiko-nagano:20190627094722p:plain
クックパッドアプリで提供している食べ方検索機能

検証の方法はシンプルで、クックパッドマートで扱う主な食材キーワードを各機能に投げ、返ってきた結果をスプレッドシートにまとめました。それを、日常的に料理をしている人にユーザー目線で見てもらい、一番グッとくる結果を返せた機能はどれかを精査しました。

結果として、一番良さそうという感触が得られたのは「定番メニュー抽出ロジック」でした。定番メニューと言えども、10件以上提案されると自分の頭だけでは浮かんでこなかった食べ方に出会うことができる実感が得られました。また「定番」なので、突飛過ぎずにイメージしやすいといういい塩梅の提案が出せることがわかりました。

仮説をサービスに落とし込む

技術的な実現目処もたち、いよいよサービスの中でかたちにしていきます。

Figmaで画にしてレビューを繰り返す

UIデザインは基本的にFigmaを使って共有しながら作っていますが、初期はパターンをとにかくたくさん出し、良さそうな案を探っていきます。

f:id:yoshiko-nagano:20190627094756p:plain
まずは手書きのスケッチから

f:id:yoshiko-nagano:20190627094822p:plain
Figmaでたくさんのパターンをつくる

クックパッドマートチームは、日常的に料理をしているメンバーが多く、部内で簡易なユーザーテストをしてみるだけでも有益なフィードバックが多く得られます。Figmaのプロトタイピング機能を使って実機でデザインを見せるということを繰り返し、デザインの方向性を固めていきました。

また、画にする <-> フィードバック のサイクルを出来るだけ短くする方法として、最近はペアデザインも試しています。UIデザイナーとPMで数時間社内の空きスペースにこもり、画にすることと議論することを同時進行で進めることで、デザインの精度とアウトプットのスピードが高められることが実感できました。

f:id:yoshiko-nagano:20190627094857p:plain
オフィスの片隅でペアデザインをする様子

実データを見られるようにする

ダミーデータでデザインをしていると、どうしてもこちらの都合の良い見え方だけでデザインが進んでしまいがちです。なので、早い段階で実データを見られるようにすることも意識しました。

レシピサービス側に必要なAPIのエンドポイントを作成し、クックパッドマートの管理画面からアクセスしてデータを取得できるようにする機能を早めの段階で実装しました。実際にどんなデータが何件返ってくるのかを確認できることで、デザインと現実のギャップを埋めることができました。

段階的に実装・リリースする

今回は、変更全体を一度にリリースするのではなく、商品詳細に「食べ方」を表示するフェーズと、アプリのホームに「食べ方」を表示するフェーズの二段階に分けて、リリースを進めました。

リリースを分けた理由はいくつかありますが、実装範囲が絞られることで既存機能への予想外の影響を少なくできることと、QAの対象範囲を狭めてQA期間を最小限にできることが大きいと思っています。

出来るだけリリーススピードを落とさず、チームが常にサービスを改善し続けている実感を持てる状態が、健全なサービス開発を進めていく上でとても大切だと考えています。

まとめ

「何から手をつけようか…?」と完全に手探りなところから、様々な手法を使って仮説を定め、一つの新機能としてサービスに落とし込むまでのプロセスをご紹介しました。

クックパッドマートは、買い物を便利にするだけのサービスにとどまらず、食材を買うことの先にある「料理をして食べる」という体験全体をデザインしていくことが重要だと考えています。おいしい食材でおいしい料理を作って大切なひとと食べる時間を世の中にもっともっと増やしていくために、今後もサービスを進化させていきたいと思います。

この記事を読んでクックパッドマートの開発にご興味を持っていただけた方がいれば、ぜひ一緒にサービスを作りましょう!ご応募おまちしております。

www.wantedly.com www.wantedly.com

Markdown と GitHub で社内規程を便利に管理

VP of Technology の星 (@kani_b) です。技術基盤や研究開発領域などを担当しつつ、社内の色々なことを技術の力でいい感じにする仕事をしています。セキュリティや AWS の話が好きです。

さて、みなさんは、ご自身が勤務する会社の就業規則を読んだことはあるでしょうか。 エンジニアに限らず、会社の全スタッフが仕事をする上で関わってくるのが、就業規則や情報セキュリティドキュメントなど、会社のルールや規程を記す文書です。 特にセキュリティやインフラに携わるエンジニアは、その改訂も含め携わったことがある方もいるのではと思います。

よくある文書管理

こうした文書は、以下のように管理されていることが多いようです。

  • ベースドキュメントは Word
    • 保存時は PDF で保存
  • 版管理は Word の編集履歴 + PDF に保存する際のファイル名
  • 編集は担当部門, 担当者のみが行う

かつてのクックパッドでも、上記のように作成された PDF ファイルを Google Drive に保存して従業員向けに公開していました。 この記事を書くにあたり他のいくつかの企業の状況を伺ったところ、細かな差異はあれど同じような運用をされている例がほとんどでした。

つらい点

上記のような管理において、自分がつらいと感じる点がいくつかありました。以下に挙げていきます。

レイアウト難しい問題

複数人で編集することを前提とした文書の体裁を Word や他のワープロソフトで保ち続けるのはなかなか難しいものです。 全員が習熟していれば良いのですが、習熟度に差があると同じレイアウトでさえ記述方法が違っていたりします。 「番号付きリストかと思ったら番号は手動入力されていた」「中央寄せかと思ったら全角スペースの数でレイアウト調整されていた」「改行の数が違うとレイアウトが崩れる」なんてことはよくある話ではないでしょうか。

そもそも、そこまで頑張って整えている体裁は本当に必要…?

版管理難しい問題

ワープロソフト側に版管理の機能が備わっていることも多いのですが、複数人での編集を前提とするとき、全員が意識して同様の管理を行う必要があります。また、担当者の引き継ぎによって文化が失われてしまうような悲しい事態も起こります。 それ以外にも、規程閲覧側に公開されるのは最終成果物である PDF ファイルのみであることが多く、差分を確認するためにはそのバイナリに対応したソフトを利用する必要があります。閲覧側にとっても便利とはいえない状況です。

これら2点が感じていた大きな問題ですが、他にも

  • 文書を横断した検索性が悪い
  • 複数人でのレビューが難しい

といった問題を感じていました。

社内文書管理に求めていたこと

ここまでに書いた問題を感じつつ、つらい〜と鳴きながら文書編集をしていたのですが、ある日雑談の中でそうした文書管理を担当していた、いわゆるバックオフィスの同僚も同じようなつらさを感じていたことを知りました。 そこで、規程や社内マニュアルなどの文書に求められることを簡単にまとめてみたところ、概ね以下のような条件を満たしていれば良いのでは、という結論に至りました。

  • 然るべき責任者の承認のもと編集されていること
  • きちんと版管理が行われていること
  • 編集すべきときにすばやく編集できること
  • 見たいとき、見るべきときにすばやく参照できること
    • 閲覧しやすいフォーマットであることが望ましい

あれ、これって…?

あえてプレーンテキストを使う

というわけで、Word と社内ファイルサーバおよび Google Drive で管理されていた社内文書を、Markdown で書かれたファイルを Git (GitHub) で管理する形に移行しました。

Markdown は、 GitHub 上でリッチな表示を使ったレビューが可能です。また Groupad と呼ばれる社内 Wiki では長年 Markdown 記法が使われていました。このため、Markdown という名前を知らなくても 「Groupad と同じ記法」と説明すれば通じる状況にあり、利用をはじめるにあたりあまり障壁はありませんでした。

f:id:kani_b:20190626175720p:plain
Markdown で書かれた就業規則

また、GitHub についても、数年前から全社員に GitHub Enterprise のアカウントが発行されており、人事部門や法務部門も Issue ベースでのやり取りに慣れているといったところから、ある程度スムーズに利用を開始することができました。

GitHub からのファイル編集

Git 移行にあたって最も障壁となりやすいのは、編集作業をどのような環境で行うか、という点だと思います。 素の Git コマンドをターミナルから使ってくれ、ではハードルはどこまでも高くなるだけですので、Git を使ったことがない同僚にはまず GitHub の編集機能を使ってもらうことにしました。 あまり利用されていないようにも思えますが、GitHub そのものにも編集機能が用意されており、ファイル編集や変更の Commit, Branch や Pull Request の作成なども可能になっています。Git の概念をすべて理解してもらうのでなく、使う機能を限定することで、極力移行をスムーズにしました。

改訂フロー

現在の文書改訂フローは以下のようなものです。

  • 担当者が改訂案を Markdown で起案し Pull Request を作成
    • 必要に応じて他の担当や上長からのレビューを受ける
  • 責任者は確認し、内容に問題がなければ承認とマージを行う*1
    • GitHub の Branch Protection を使い、責任者の Approve がなければマージできないようになっている
    • 会議体の承認が必要な文書は Pull Request のスクリーンショット (記録のため) ごと会議体にまわり承認されたのち、責任者によって Approve される
  • マージ後は自動的に公開される

GitHub を使った開発でも行われるようなフローで文書管理を行えるようになっています。

文書の公開

GitHub における Markdown ビューを使っても、文書として読むのに問題ないビューを得ることができます。 ですが、文書の移行をすすめるうち、複数ある社内文書のインデックス作成やカテゴリ分類、少し複雑な計算式表現などが必要になりました。*2

そこで、Markdown が利用可能な静的サイトジェネレータである Jekyll を使い、Markdown で作成した文書から静的ページを生成して社内に公開しています。ドキュメント用の Jekyll テーマである tomjoht/documentation-theme-jekyll を採用しました。 Jekyll は GitHub Pages によって GitHub 側でビルドを行えるので、作成した文書をそのまま提供することもでき便利です。

当初 GitHub Pages による提供を考えていたのですが、例えば入社が内定された (入社前の) 方に公開するなど、アクセス制御の要件が増えてきたため、現在では Jenkins 上で自動ビルドを行って Amazon S3 上でホストしています。

f:id:kani_b:20190626175943p:plain
Jekyll でビルドされ公開されている就業規則

クックパッドでの利用状況

現在クックパッドでは、就業規則や賃金規程などほぼすべての社内規程が上記のような形で管理されており、法務部門や人事部門も GitHub を使った文書管理を行っています。 従業員はビルドされた HTML ドキュメントを参照することも可能ですし、元の Markdown を確認することや、Git のコミットログを確認することも可能です。

情報セキュリティのガイドブックといった、改訂しやすい文書は担当者外からの Pull Request も受け付けています。レイアウトの修正のほかに、ルールそのものに対する提案も Pull Request として来るものが生まれており、議論しやすくなったと感じています。

また、文書全体のレイアウトを整えやすくなり、より構造を意識して書くようになったり、従業員からの検索性が上がったり*3といった効果もありました。

あくまで個人的な感想ですが、編集しやすくなったことで、改訂に対する (気持ち的な) 腰の重さも軽くなったように思います。規則やそれを記述する文書は従業員の業務を助けるものであり、会社や世間の状況が変わった際すばやく改訂できる状態を保つことはとても重要と考えています。

まとめ

クックパッドにおける社内規程の文書管理を Markdown および GitHub を使った管理に移行した事例についてご紹介しました。

担当者全員が Git を覚えなければならない世界にするのでなく、GitHub といったある程度親しみやすいインタフェースを間に利用するといったように、担当者や閲覧する従業員にとって良い形を追い求めていくことは非常に重要です。こうした改善は「やりたい人の独り善がりにならないようにすること」がとても大事な、かつ楽しい部分だと感じています。

また、クックパッドでは、今回ご紹介したように技術を活用しながら全社の業務をより良くしたいコーポレートエンジニアや、自らの領域を一緒に改善していける財務・人事といったコーポレート部門スタッフを大募集しております。興味をお持ちいただけましたら、キャリア採用情報 (https://info.cookpad.com/careers/jobs/) から詳細を是非ご確認ください。

*1:軽微な修正のため、担当者の判断でマージできる文書もあります

*2:たとえば、賃金規程において、賃金の計算式を示すのに MathJaxを使っています

*3:現在は GitHub における検索にまかせています

SwiftUIで使用されているSwift5.1の新機能

こんにちは。会員事業部の岡村 (@iceman5499) です。 普段はクックパッドアプリ(iOS)を開発しています。 先日San Joseで開催されたWorldwide Developers Conference 2019 (WWDC19)に参加し、そこでSwiftUIの発表をうけていくつか調べたことがあるので簡単にまとめておきたいと思います

SwiftUIの登場

今年のKeynoteの最後に、SwiftUIという新たなUIフレームワークが発表されました。 SwiftUIはReactやFlutterのような形式でViewを宣言して画面を構築できる、これまで使用されてきたUIKitとは全く異なる形式のフレームワークです (AppleのSwiftUI紹介ページ )

この発表をうけてKeynoteはとても盛り上がっていました。期間中もSwiftUIの話題でもちきりで、セッションも多く開かれていました

SwiftUIでできるようになること

  • DSLでViewを宣言的に適宜できるようになりUIの構成要素を簡単に表現できるようになった
  • コード編集中にリアルタイムにUIプレビューを利用できる *1
  • 余白調整やアクセシビリティ・ダークモード対応などがある程度自動で行われ、Human Interface Guidelinesに則った画面を作成しやすい
  • スムーズなアニメーションが簡単に設定できるようになった

UIKitではよくあるリスト形式の画面を作るだけでも TableViewDelegateTableViewDataSource のメソッドを多数実装したり、ラベルを上下に並べるのに

label.constraint(equalTo: otherLabel.topAnchor, constant: 16).isActive = true

などといった長いコードを書いていく必要がありましたが、SwiftUIではそれがすっきりして

 List(contents) { content in
    VStack {
      Text(content.title)
      Text(content.subtitle)
    }
 }

のような形でシュッと書けるようになりました 🎉

f:id:iceman5499:20190624145219j:plain
Introducing SwiftUI: Building Your First App

(Introducing SwiftUI: Building Your First App より)

実際にさわってみた感想

2019年6月現在。macOS Catalina 10.15 Beta 2 と Xcode 11 Beta2 を使用しています

プレビューめちゃくちゃ使いやすい!?

  • 起動して目的の画面にたどり着くための操作をしなくてもいい
  • その場でタップフィードバックなども試せる
  • モックデータを簡単に挿せる

あたりの機能は非常に便利で、今後のプロトタイピング開発やエラー表示のテスト、デザインドキュメントとしての利用など様々な場面での活用が予想されます

新規プロジェクトならいい感じに動いたのですが、一方で既存プロジェクトで動かそうとした場合にいくつかの問題点に遭遇しました

  • ビルドターゲットがiOS13未満に設定されているとプレビュービルドができない *2

Swiftでは @available を用いることで指定コードが有効化されるiOSバージョンを制御することができます。これを用いてビルドターゲットがiOS13未満のプロジェクトにおいては

@available(iOS 13.0, *)
struct ContentView : View {
    var body: some View {
        Text("Hello World")
    }
}

のように記述をすることでビルド及び実行することができるようになります(iOS13未満ではSwiftUIを使用できないため代わりの実装を用意する必要があります)

こちらの書き方を使用して既存プロジェクトからSwiftUIを利用する場合、Xcode11Beta2時点ではプレビュービルドはエラーとなり利用することができませんでした

  • Objective-C製ライブラリ(Firebaseなど)を使ってるとたびたびそれらのビルドが走る

これはSwiftUIの問題ではなくビルドシステムの都合だと思うのですが、プレビューを使用する際は変更部分のみがリビルドされるはずがObjective-Cを利用している場合にそれらのビルドが走ることがあり、現実的な待ち時間でプレビューを使用することが難しいことがありました

  • 新規プロジェクトでも急に止まったり調子悪くなったりしがち

こちらはシンプルにプレビューの描画が止まったり明らかにおかしくなったり、ビルドが長くなったりなどです

と、このような障害もあり、まだBetaであるため安定してないのはしょうがないですが、安定しないままリリースされる可能性も十分にあり得るため今のところプレビューはあくまで補助的なものと捉えています。

個人的にあるだろうと思った機能がないこともある

触っていくとだんだんと気づくのですが、Betaということもあって個人的に必要だと思った機能が実は存在していないといったケースがあります

  • HStack などを用いて複数のViewを等幅で配置できない *3
  • ボタンハイライト時の挙動を設定できない *4
  • 画面を閉じる or 戻るボタンを配置できない

コンポーネントはどんどん拡充されていくはずですので、リリース時点やその後のコンポーネントの拡充に期待です

SwiftUIで使用されているSwift5.1の新機能

SwiftUIにはSwift5.1で新規追加される機能がふんだんに使用されていました

  • @propertyDelegate
  • @_functionBuilder
  • Opaque Result Type
  • @_dynamicReplacement(for: )
  • KeyPathに対する @dynamicMemberLookup

順にみていきます

@propertyDelegate

Proposal: SE-0258

(※ Proposalでは Property Wrappers という命名になっていますが、Xcode11Beta2上ではまだ @propertyDelegate が使用されているため本記事ではこちらで表記します)

この修飾子をつけるとプロパティに対して新しいattributeを宣言できるようになります

例えば次のような Lazy を宣言してみます

@propertyDelegate enum Lazy<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)

  init(initialValue: @autoclosure @escaping () -> Value) {
    self = .uninitialized(initialValue)
  }

  var value: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      self = .initialized(newValue)
    }
  }
}

このような @propertyDelegate が宣言されているとき、次のようにその宣言された型の名前でattributeを宣言できるようになります

@Lazy var foo = 1738

これは実際のコンパイル時に以下のように展開されます(イメージのための疑似コードです)

var _foo: Lazy<Int> = Lazy<Int>(initialValue: 1738)
var foo: Int {
  get { return _foo.value }
  set { _foo.value = newValue }
}

foo へのアクセスが _foo に移譲される形となり暗黙に Lazy<Int> の機能を利用できるようになります。Lazy では遅延初期化の実装がされているため、lazy var と同じような機能が @Lazy をつけることによって利用できるようになりました

Property Delegateを使用したSwiftUIの型は多数存在しています。 例えば @State ではViewに使用される値の更新検知をしており、View.body の中で @State つきの変数にアクセスするとその変数の監視が始まり、その値が変化したときに自動的にViewが更新されるといった挙動をみせています

また $ をつけることによってラップしている本来の型のオブジェクトにアクセスすることができます

$foo // → Lazy<Int>

このとき、さらにラップしている型に var delegateValue: T { get } が定義されていれば delegateValue を取り出すことになります

例えば @State では var delegateValue: Binding<Value> { get } が定義されている*5ため、

@State var inputText: String

...

var body: some View {
  // ↓ TextField.init(_ text: Binding<String>) に対して
  //   $inputText.delegateValue: Binding<String> を $inputText という記法で取り出して渡している
  TextField($inputText)
}

次のようなコードがある場合に $inputTextBinding<String> を返します。 TextField は自身への入力を Binding<String> を経由して別のところへ渡すというインターフェースをしています

ややこしいですが、これによってSwiftUIはViewへのデータバインディングのためのプロパティの更新検知を実現しています

@_functionBuilder

Forum、 Proposal: SE-XXXX

VStack {
  Text("Hoge")
  Text("Fuga")
}

このコードを見たときSwiftのエンジニアは当然 🤔となると思います。クロージャが返り値を持っておらず、途中で評価しただけの Text が何らかの形でクロージャの外に現れています

VStackのイニシャライザ(一部省略)はこうなっていて

init(@ViewBuilder content: () -> Content)

なるほど怪しい @ViewBuilder が生えてることがわかります

これは新たに追加された @_functionBuilder による機能で、どこかに @_functionBuilder struct ViewBuilder {} が宣言されているときクロージャ引数に @ViewBuilder を付与できるようになり、そのクロージャの中で評価された式は ViewBuilder が持つ各種build関数の中を通って出力されます

例えば中で2つのViewが評価されていたときはViewBuilderの public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View 関数の中を通ります(1つ目に評価されたViewがc0、2つ目がc1として引数が与えられます)。結果、クロージャは TupleView という型のインスタンスを返します

Swiftにこのような言語機能が搭載されたことによって、Swiftの型検査を有効にしたままDSLが記述できるようになっています

Opaque Result Type

Proposal: SE-0244

これまで関数の返り値にprotocolを指定した場合はexistential type *6 にラップされて返され、ラップやアンラップの処理にオーバヘッドが発生していました。 またprotocolがassociated typeを持っていた場合はprotocolはGenericsの型パラメータを持てないので AnyHashable など型消去のテクニックを用いて返却する必要がありました。これは元の型の情報を失っているために本来比較可能でないもの同士を比較できてしまうなどのコードを記述できてしまいました

Swift5.1からその問題を解決するために、 "protocol P を満たすある一つの型" を返すという意味で some P という表現ができるようになりました。これによってPを満たす任意の型を実際の型を知らずとも扱えるようになりました

これがどのように作用しているかというと、 多くのSwiftUIの構造体は

struct Button<Label> where Label : View {

のようにGenericsでそのViewの内部にあるViewの型を指定して受け取ります。existentialはそれ自身のprotocolに適合しないのでexistential経由でGenericsへ型パラメータをわたすことはできず、この型パラメータのためにはconcreteな型を知る必要があります

ただしSwiftUIの型は非常に複雑で、例えば上の

VStack {
  Text("Hoge")
  Text("Fuga")
}

VStack<TupleView<(Text, Text)>> 型です。 これはまだマシですが、

List {
    Section {
        ForEach(names.identified(by: \.self)) { name in
            Text(name)
        }
    }
}

だと List<Never, Section<EmptyView, ForEach<IdentifierValuePairs<Array<String>, String>, Text>, EmptyView>> 型になります。 こんな複雑な型をいちいち返り値に記述することは人間には難しいですし、変更に弱すぎます

そこで、それらをまるごとひっくるめて some View として表現できるようになっています。 このような表現をSwift proposalでは、Opaque Result Typeと説明しています

var body: some View {
    List {
        Section {
            ForEach(names.identified(by: \.self)) { name in
                Text(name)
            }
        }
    }
}

実際のコードはこうなので、上のような複雑な型を書く必要がなくなっています。 この機能によって実装中に複雑な型の存在を意識せずともViewを取り扱えるようになっています

@_dynamicReplacement(for: )

Forum

これはXcodeでのプレビュー用に使われている属性で、dynamic 修飾子がついた関数などにこの属性がついたモジュールをロードしてあげるとその関数の実装を入れ替えることができるようになります。 SwiftUIのPreviewではこれを用いて実行中のシミュレータが持つバイナリの実装を動的に差し替えてリアルタイムなプレビューを実現しています

ちなみにこの挙動の存在は、Preview機能がクラッシュしたときのエラーログからXcodeがプレビュー対象のコードに @_dynamicReplacement(for: ) をつけて回っていてあとから差し替えてる様子が確認できたことから確認しました

さながらObjective-C時代のMethod Swizzlingですね

KeyPathに対する @dynamicMemberLookup

Proposal: SE-0252

@dynamicMemberLookup は以前からSwiftに実装されている機能ですが、今回新たにKeyPathに対してsubscriptできるようになりました。 具体的な定義はこんな感じです

// BindingConvertibleの例
subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Self.Value, Subject>) -> Binding<Subject> { get }

任意のKeyPathをdynamicMemberLookupできるようになったため、プロパティアクセスのふりをしつつ型安全にsubscriptでアクセスできるようになりました。 これが具体的にどういうことか、以下のコードをみてみましょう

@dynamicMemberLookup struct Box<T> {
    var value: T

    subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> U {
        return value[keyPath: keyPath]
    }
}

struct User {
    var name: String = "taro"
    var age: Int = 42
}

let boxedUser = Box(value: User())
print(boxedUser.age) // → 42

boxedUser.age はいかにも Box に生えているように見えますが、実際にアクセスする先は User の持つ age となっています。 このようにして、 @dynamicMemberLookup を使用することで subscript で指定されてる型に適合するkeyPathを \.age などの記法を使わずに取り出してあたかもプロパティ呼び出しであるかのようにsubscriptに流し込んで呼び出せるようになっています

これはSwiftUIではprotocolの BindableObject で活用されており、

struct ViewModel: BindableObject {
  var name: String
  ...
}

@ObjectBinding var viewModel: ViewModel

として宣言されている viewModelに対して、

TextField($viewModel.name)

のように $viewModel.nameBinding<String> として取り出す操作を可能にしています。 (ObjectBindingのdelegateValueは ObjectBinding<BindableObjectType>.Wrapper 型であり、それはKeyPathのdynamicMemberLookupで Binding<T> を返す) 一見viewModelに生えてるStringのプロパティを取り出しているように見せかけてBindingを返せているのでpropertyDelegateの恩恵をぶら下がってるプロパティにも適用できるようになっています

まとめ

SwiftUIで使用されているSwift5.1で追加された新機能について調べてみました。 マイナーアップデートでありながら大胆な機能が多数追加されてコードの様子が一気に様変わりしましたね。見た目は大きく変わりつつも中身は型の効いてるSwiftらしさがあり挙動や実装を調査していくのはとても楽しいですね

クックパッドアプリ(iOS)は1年前のiOSバージョンまでサポートする運用をしており、なんとあと1年とちょっと待てばSwiftUIが実用段階になる予定です。 また新規アプリを作成する際は最初からSwiftUIでやっていけるかもしれません。 クックパッドではSwiftUIを使ってすばやくサービス開発していくエンジニアや、SwiftやXcodeに詳しく開発環境を改善していけるエンジニアを募集しています

https://info.cookpad.com/careers/

*1:正確にはXcode11&macOS Catalinaの機能

*2:Beta2時点

*3: 内部を HStack { Spacer(); content(); Spacer() } で囲む、GeometryReader でframe直打ちなどのやり方はありますがすっきりするものではありません

*4:longPressAction や DragGesture を使うという裏技もありますがすっきりするものではありません

*5:https://developer.apple.com/documentation/swiftui/state/3287851-delegatevalue

*6:https://blog.waft.me/2017/10/27/swift-type-system-08/ などで詳しく解説されています

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/