近頃、昨年の発表直後にApple公式のチュートリアルを触って以来久しぶりに、本格的にSwiftUIを触り始めた。
趣味プログラミングはさておいて、SwiftUIを実案件に導入することを考えた際、まず気になったのは「実際の所どれほどの表現力があるのか」であった。そこで、上述した公式チュートリアルや、そのほかの書籍を読みながら試行錯誤していくのだが、、
そんな中で気がついた事は、UIKitのコンポーネントをSwiftUIで再現することによって、SwiftUIへの理解が加速し、加えて(現状の)SwiftUIに「できないこと」を炙り出せるのではないか、ということだった。
最終目標が常に明確でありながら、UIKitで実装されているがゆえにSwiftUIでは一筋縄に行かない、つまり試行錯誤を要するという点では、多角的かつ効率的に理解を深められると実感しはじめている。
そこで、UIKitのコンポーネントをいくつか例にとって、SwiftUIによる「写経」を試み、SwiftUIにおける表現力向上に挑戦してみたいと思う。
今回は、UISegmentedControl
。iOS 13以降デザインが一新されたコンポーネントのひとつで、従来の簡素さから表現力がおおきく飛躍した。

そして出来上がったのがこちら。

この実装における、いくつかのポイントを紹介してみる。(投稿の最後に実装全体へのリンクも貼り付けた)
- ドラッグジェスチャ(drag gesture)を扱う
ドラッグジェスチャ(drag gesture)の現在位置のx座標を、項目ごとの幅(cellWidth)で割ることで、何番目の項目が選択されているか(index)を計算することができる。
項目ごとの幅 = 全体の幅 / 項目数
選択インデックス = 現在位置x / 項目ごとの幅
しかしここでのつまずき所が全体の幅
、すなわちSegmentedControl自体の幅である。UIKitのようなself.frame
が提供されていないSwiftUIでは、こうした自明に思われる幾何情報すら取得に難儀するのである。
そこで活躍するのがGeometryReader
。ここから得た値を、自身のプロパティ@State var width: CGFloat
に保持するようにした。
PreferenceKey
で監視した bounds
の変化に応じて、GeometryReader
で得た geometryProxy
を任意に取り扱う処理を、extension View
として実装し共通化した。
たかだか自身のサイズを取得するためだけに、ここまで大仰な実装が必要なのは、かなり冗長に感じる、、より簡潔に実装する方法があれば知りたいのだが。
2. 階層内の他ビューの幾何情報を扱う
SegmentedControlにおいて、選択中の項目をハイライトするためには、その項目のframe
を知る必要があるのだが、前述の通り、SwiftUIにおいてはUIKitのように都合良くはいかない。
そこで、AnchorPreference
の仕組みを利用して、選択済み項目のbounds
を上位階層に伝播する。
ここからは、今回の実装で気がついた、投稿時点のSwiftUIではどうしても取り扱えなかった仕様について記す。その大きなひとつとして
- タッチダウンジェスチャを検知できない
というものが挙げられる。これにより、タッチダウン時の視覚効果はSwiftUIのみでは再現することができなかった。
※ SwiftUIのみに拘らなければ、UIViewPresentable
を活用する方法がある。
以下に貼り付ける実装の全容は、試行錯誤を経てようやくたどり着いたひとつの答えなのだが、よりシンプルに実装する方法はありそうだ(寧ろあって欲しい)。
いずれにしてもここで伝えたかったことは、あえて非SwiftUI的な実装目標を置くことにより、SwiftUIで何ができ、何ができないのかを知る一助になった、ということだ。それは標準コンポーネントでも良いし、3rdパーティアプリを真似てみるのでも良さそうだ。