技術本部 Mobile Application Groupの栗山です。普段はSansan iOSアプリの開発を担当しています。
突然ですが、技術評論社さんから絶賛発売中の雑誌「WEB+DB PRESS Vol.126」のiOS 15 開発最前線
というiOS 15を題材にした特集で、共著者の一人として第3章の執筆を担当しました!
商業誌に掲載されたのは人生ではじめてのことだったので、出来上がった雑誌を手に取った時は本当に嬉しかったです。
今回は、知っていると iOS 15開発最前線
をより楽しめるような補足解説や、後半はUISheetPresentationControllerをSwiftUIで使うために行った試行錯誤についてご紹介したいと思います。
目次
エンジニア以外の方にも読んで欲しいiOS 15特集の記事
iOS 15特集の特徴の一つは、実装方法の紹介だけに留まらず、開発環境やストア関連も含め広範囲な内容がわかりやすく紹介されている点だと個人的に考えています。
中には、エンジニア以外のプロダクトに関わる方にも参考になる内容があります。いくつかピックアップしてご紹介したいと思います。
第5章 通知管理の新機能
iOS 15になって、Push通知周りの仕様が大きく変わりました。時刻指定要約や集中モードといった新機能により、場合によってはこれまでと通知の体験が大きく変わることが有り得ると思います。
対応しないとストア審査が通らないといった類の変更ではないので、プロダクトによっては対応の優先順位が上がりにくいものがあるかもしれませんが、上手く対応することでアプリによっては利便性が向上したり、よりユーザーが満足する形で通知を送ることができる可能性も秘めています。
iOS 15でどのように通知が変わるのかが読みやすい分量でまとまっているので、PdM (プロダクトマネージャー)の方やユーザ体験を検討する役割を担っている方にも読んで欲しい内容だと思います。
第7章 App Store の新機能
詳細は特集を読んでもらうとして、App Store上で掲載される情報のカスタマイズや期間限定のイベント開催もやりやすくなりました。マーケティングを担当している方、特にASO (アプリストア最適化)を考えている方にも読んでみて欲しい内容だと思います。
原稿執筆時点では一部機能がまだリリースされていない状態でしたが、リリースされていなかった機能のうち、プロダクトページのカスタマイズは昨年の12/8にリリースされ、利用できるようになりました。
また、年明け早々のアナウンスでAppアナリティクスでApp内イベントに関する情報も閲覧できるようになったようです。
入稿するまでの歩みを振り返る
原稿を書くことになったいきさつ
今回の執筆は、 iOSDC Japan という日本最大規模のiOS開発者向けテックカンファレンスで、個人的に紙の本として作品を形に残してみたい一心で、iOSDC Japanのパンフレット原稿のプロポーザルを出したことがきっかけです*1。
ありがたいことに、2020年(2本)と2021年(2本)と、2年連続で自分が応募したプロポーザルが採択され、自分の書いた原稿が合計4本掲載されました(これも紙のパンフレットが届いて手に取った瞬間は大変嬉しかったことを覚えています)。
iOSDC Japan 2021のパンフレット原稿を提出し終えて半月ほど経過したある日のこと、iOSDC Japan 実行委員長の長谷川さんから、「WEB+DB PRESSのiOS 15特集の執筆を担当してみませんか?」とのお誘いを受けました。お誘いを受けた直後は大変びっくりしましたが、自分のパンフレット原稿をご覧いただいた上でお誘いいただいたようで、お誘いを受けたこと自体大変嬉しかったです。
また、「WEB+DB PRESS」は、学生の時から読んでいた雑誌で、そんな雑誌に自分の書いた原稿が掲載されるチャンスが訪れるとは思ってもみませんでした。
こうして、共著者として原稿の執筆活動がスタートしました。
入稿までの執筆プロセスについて
商業誌向けの原稿執筆が始めての自分にとって、原稿を作り上げるまでのプロセスはこれまで経験したことがないことの連続でした。
大きく分けて、次のようなプロセスがありました。
- 各章のテーマと担当者を決定する
- 担当する章のアウトライン(見出しの構成)を考える
- アウトラインについて編集部の方と共著者間で相互レビューを行う
- 本文を執筆する(図表も併せて作成する)
- 編集部の方と共著者間で相互レビューを行う
- 雑誌の簡易レイアウトに本文を流し込んで、分量の調整や相互レビューを複数回行う
やはり、紙の原稿というのは一度印刷してしまうと訂正が容易でない、というのもあってか、チェック体制もしっかりしていたように思います。著者としてもレビューアとしても独特の緊張感がありましたが、これらのプロセスを経たからこそ、完成度の高い記事が完成するのだと、改めて納得しました。
執筆環境、レビュー体制
執筆環境
原稿はGitHub上で管理していました。図表については別なツールで編集したものをリポジトリにアップロードする形でした*2。
本文については、ほぼMarkdownそのままの記法で書くことが出来たので、頻繁にMarkdownを書いている身としては非常に作業しやすかったです。
ただ、Markdownとして利用可能な太字(**太字**
)などの一部記法については、例えばコード中でMarkdown記法と同じ記法を使っていた場合には、意図せず太字に変換されてしまうことがあります。その場合は、別途編集の方宛に本文中で指示を記載していました。
レビュー体制
執筆の進め方としては、共著者ごとにブランチを切ってどんどん原稿をPushし、Pull Requestを作って相互にレビューする形を取っていました。こちらも、普段の開発と大差ないやり方で執筆作業を行えたのは個人的には良かったです。
執筆プロセスのところで書いた通り、編集部の方や共著者間で何度もレビューを行いましたが、他の共著者の方が普段どういった形でレビューしているのかを垣間見ることができたのが良かったです。
また、(ありがたいことに)これまで内容に関するフィードバックは日常的にもらえる機会がありましたが、今回は日本語としての言い回しに関するコメントも多くあり、私自身とても勉強になりました。
原稿執筆Tips
原稿執筆にあたって、個人的に工夫していたことについて簡単にご紹介したいと思います。
個人的なタスク管理について
特に本文の執筆が始まって以降は、本文の執筆の他にも共著者間でのレビューやレイアウトの検討など、様々なタスクが発生しました。
自分が思っていた以上にタスクが多いことに途中から気づいて以降、タスクの実行漏れを防ぐために執筆関連のタスク管理をTrelloを使って管理していました。
Markdownのコメントの有効利用について
Markdownを書いている際にコメントアウト(レンダリング対象から外したい)的なことをしたい場合は、HTMLライクに <!-- ここにコメントを書く -->
と書いたり、もしくは [](ここにコメントを書く)
と書くことが多いのではないでしょうか。これらコメントアウトは原稿執筆時のフォーマットでも有効になっており、自分はメモ代わりに使用していました。
例えば、図表はMarkdown上の画像を貼り付けるときと同じ記法で記載しますが、図表番号は自動付与されるため、本文中で図表番号を指定する際の誤りを防ぐために、想定される図表番号をコメントで入れておいたりしました。
また、 担当する章のアウトライン(見出しの構成)を考える
フェーズにおいては、見出しの数が多すぎず少なすぎないことが求められるため、見出しの数を原稿の節毎にコメントに記載してバランスを調整したりしました。
日本語の書き方に関する参考文献
編集者の方からいただいた、原稿執筆時の注意事項に関するドキュメントを読んでいたところ、日本語の作文技術 という書籍が紹介されていました。
早速読んでみましたが、係り受けの話など、参考になる話が沢山載っていました。
ただ、個人的には(良くないとは思いつつも)あまり時間がない中でもう少しだけ手っ取り早く重要な部分だけキャッチアップしたいという欲望が強くなってきました。そう思っていたある日、同じ著者の方が 中学生からの作文技術 という本を出版しているというのを発見しました。内容的には先の書籍の内容に近かったため、自分は途中からこちらの方を読んでいました。
いずれの書籍も、自分の文章の書き方をアップデートするのに有用だと思いますので、読んでみてはいかがでしょうか。
UISheetPresentationControllerをSwiftUIで使ってみる
ここからは、実装を交えて特集で紹介したトピックの深掘りをしていきたいと思います*3。
特集の第4章では、UIKitの新機能のうち、UISheetPresentationController (リサイズ可能なハーフモーダル)とUIButtonの新機能が紹介されていました。
UISheetPresentationControllerと同じAPIがSwiftUIには追加されず、SwiftUIをメインに使っている方にとっては、対応するまで待てないと思う方もいるのではないでしょうか。ただ、SwiftUIにはUIKitで実装したUIをSwiftUIで利用する仕組みがあるので、ここではその仕組みを使ってSwiftUIでUISheetPresentationControllerを使えるかどうか試してみたいと思います。
なお、UISheetPresentationControllerの使い方については、特集の第4章をご覧ください。
また、本章の執筆に当たってサンプルコードを作ったので、実装が気になる方は下記をご覧ください。
UIKitで実装したUIをSwiftUIで使うには?
UIKitで実装したUIViewやUIViewControllerをSwiftUIで使うには、それぞれ UIViewRepresentable と UIViewControllerRepresentable といったWrapしてSwiftUIで使えるようにするための仕組みを利用してSwiftUIで使えるようにしていきます*4。
ただ、当初は簡単に実装できると思ったものの、実際にやってみたら結構複雑になってしまいました。。
複雑になってしまった要因としては、次の2つが挙げられます。
要因その1: UISheetPresentationControllerの取得方法
UISheetPresentationControllerはUIViewControllerのプロパティになっており、使用する場合は下記のようにシート表示させたいUIViewControllerからUISheetPresentationControllerを取ってくる必要があります。
if let sheetPresentationController = viewController.sheetPresentationController { // 各種設定を行う }
つまり、UISheetPresentationControllerを使うためには、表示するコンテンツが含まれるUIViewControllerが必要になるというわけです。
また、ハーフモーダル表示をするためには、UIViewControllerの present(_:animated:completion:)
メソッドを実行して画面遷移する必要があります。そのため、画面遷移するためのUIViewControllerも必要になります。
要因その2: UIHostingControllerを使ってUIViewControllerに変換する必要がある
要因その1と関連しますが、UISheetPresentationControllerを使うためには表示するコンテンツが含まれるUIViewControllerが必要になります。仮に表示するコンテンツがSwiftUIで実装されたものだった場合、そのままだとUIViewControllerではないので、UISheetPresentationControllerを使うことができません。
上記のような問題を解決するために、UIViewRepresentableやUIViewControllerRepresentableとは反対の SwiftUIのViewをUIViewControllerへ変換する
ための仕組みである、UIHostingControllerを利用します。
final class SheetHostingViewController<Content: View>: UIHostingController<Content> { ... } ... let view = DatePicker("...", selection: $date) let viewController = SheetHostingViewController(rootView: view)
これで、SwiftUIのViewからUISheetPresentationControllerを取得できるようになります。
何をすれば良いか?
まとめると、SwiftUIでUISheetPresentationControllerを使いたい場合は
- UIViewControllerRepresentableを使ってUIViewControllerをWrapしてSwiftUIから使えるようにする
- SwiftUIのViewをUIViewControllerへ変換するためにUIHostingControllerを使ってViewをWrapする
- UIHostingControllerでWrapしたViewからUISheetPresentationControllerを取り出し、設定を行う
- ハーフモーダル表示するためにUIViewControllerを用意する
の4つが必要になります。
DatePickerをリサイズ可能なハーフモーダル表示してみる
では、試しにSwiftUIのDatePicker構造体をリサイズ可能なハーフモーダル表示してみたいと思います。
・・とはいえ、実装を全て見せると文章量が大変なことになってしまうので、具体的なコードは章の冒頭で紹介した GitHubのリポジトリ を参照いただければと思います。
実装にあたり、注意しないといけないこととしては、SwiftUIでリサイズのできないハーフモーダル表示は既に sheet(isPresented:onDismiss:content:)
修飾子が存在していますが、リサイズ可能なハーフモーダルについては、まだ修飾子が存在しないため、自前で修飾子を実装する必要がある、ということです。サンプルでは次のような実装を行いました。*5
public extension View { func sheetPresentation<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content: View { background { SheetPresentationController(isPresented: isPresented, onDismiss: onDismiss, content: content()) } } ... }
実装方針としては、次の通りとなります。
- リサイズ可能なハーフモーダル表示のためのView (UIViewControllerRepresentableに準拠)を実装する
- ハーフモーダルの中身となるViewをイニシャライザで外から指定できるようにする
- 指定されたViewをUIHostingControllerでWrapする
- UIHostingControllerからUISheetPresentationControllerを取得してハーフモーダル表示のための設定を行う
- 内部で別途UIViewControllerを生成し、それを使って
present(_:animated:completion:)
メソッドを呼び出す
- で作ったViewを呼び出すためにViewに画面遷移のための修飾子を実装する
実装はGitHubを見てもらうとして、実行すると下記のような表示になります。
PHPickerViewControllerをリサイズ可能なハーフモーダル表示してみる(ただし注意事項あり)
では次に、PHPickerViewControllerのリサイズ可能なハーフモーダル表示にチャレンジしてみましょう。
PHPickerViewControllerはUIViewControllerに準拠したクラスです。DatePickerをハーフモーダル表示した際に使った仕組みをそのまま使う場合、Viewを指定する必要があるため、PHPickerViewControllerをUIViewControllerRepresentableでWrapしてあげないといけません。
というわけで、PHPickerViewControllerをUIViewControllerRepresentableでWrapしたViewを指定するようにして表示してみましたが、表示自体は上手くいくのですが、ハーフモーダルをリサイズする際に動作が非常にもっさりして使い物になりませんでした。。
実装を色々と調べたところ、どうやらUIViewControllerをUIViewControllerRepresentableでWrapしたものをUIHostingControllerでさらにWrapしていたことが原因のようでした。ネット上で具体的な情報はまだ見つけられてないですが、無駄にWrapする層が増えるので、直感的には理解できるものだとは思います。
というわけで、PHPickerViewControllerについては、UIHostingControllerを使わずにPHPickerViewControllerのままハーフモーダル表示のためのViewに渡す必要があるので、DatePickerの表示の時とは別なViewを作って表示するようにしました。結果、もっさり感もなくなり、普通に表示されるようになりました。
・・とは簡単にはいかず、特に実機で表示した時には下記のようにエラーが発生するようことが頻発するようになりました。。
調べた限りでは良い解決策は見つかりませんでした*6。恐らく、Viewを動的に動かすようなケースで発生するように感じたので、リサイズ可能なハーフモーダル表示との相性が良くないのかもしれません。リサイズのできないハーフモーダル表示を使うなど他の手段で表示することをお勧めします(iOSのバージョンが上がって解消されることを祈りたいと思います)。
(追記) 自分が紹介した以外の実装例について
この記事をチームメンバーに見てもらった際、他にも実装例があるとのことで、紹介してもらいました。ここでまとめてご紹介したいと思います。
- Using UISheetPresentationController in SwiftUI
- UIButtonをUIViewRepresentableでWrapし、UIHostingControllerを使って表示する際には
UIButton.window?.rootViewController?
といった形でUIViewControllerを取得して画面遷移する手法のようです。修飾子を使わずにViewに見立てて使用する使い方になっており、参考になりました。
- UIButtonをUIViewRepresentableでWrapし、UIHostingControllerを使って表示する際には
- BottomSheet
- ハーフモーダル処理をライブラリ化して誰でも使えるようにしたものです。SwiftPM対応してるので、試しやすくて良いと思います。
- 内部実装的には、UIApplicationを使って遷移元のUIViewControllerを取得した上で、UIViewControllerRepresentable等でWrapせずに画面遷移を実現しているようです(SwiftUIのViewはUIHostingControllerでWrapしているようです)。自分が思いつかない方法だったので参考になりました。
- isowordsのハーフモーダルの実装例
- UISheetPresentationControllerを使わずに、SwiftUIのみで実装したハーフモーダルの実装例が含まれています。UIKitを使わなくて済むメリットがあるのと、iOS 14でも使えるので、iOS 15より前のバージョンで利用したい時に参考になるかもしれません。
- ここでご紹介した実装例を私に紹介してくれたメンバーがZenn上に書き残したisowordsのメモがあるので、こちらを読んでからコードリーディングすると効率よく読み進められると思います!
おわりに
今回の執筆の話については、ここでは書き尽くせないくらい、多くのことを経験することができました。共著者の皆さま、編集部の皆さま、iOSDC Japan実行委員長の長谷川さんには、感謝の気持ちしかありません。本当にありがとうございました!