新機能の追加で、柔軟なレイアウト実装が可能に
Gridによる水平方向+垂直方向を考慮したレイアウト
iOS 16からはGridと呼ばれる新しいコンテナビューが追加され、より柔軟なレイアウトを組むことが可能になりました。
GridコンポーネントはLazyではないため、全てのViewを一度にロードします。そのため、行と列の両方にわたって自動的にセルのサイズと位置を調整することが可能です。これにより、Gridではサブビュー全体の中でその最大サイズのViewの表示スペースを確保しながらレイアウトが行われます。
上記の特徴をもとにして、Gridは「最も大きなViewに合わせて最適なレイアウトを組む」といったユースケースで強力な効果を発揮します。
具体的な例を通してどういうことかを細かく見ていきましょう。例えば下記のようなレイアウトのViewを縦に積んでいきたい場合、従来のやり方ではVStackとHStackを組み合わせて実現することが一般的でした。
これをSwiftUIで実装すると下記のようなコードになります。
var body: some View { VStack { row(name: "鮭", progress: 0.5, quantity: 25) row(name: "昆布", progress: 0.2, quantity: 9) row(name: "明太子", progress: 0.3, quantity: 16) } } func row(name: String, progress: CGFloat, quantity: Int) -> some View { HStack { Text(name) ProgressView(value: progress) Text("\(quantity)") } }
このように、左右に配置したTextは文字列の長さに応じて最低限必要なサイズに収まった形でレイアウトされます。そのため、ProgressViewの始点と終点が揃った位置になっておらず、行ごとに別々の長さになっていることがわかるかと思います。この実装を、Gridを利用して、垂直方向のサイズを考慮した上で全てのViewが等幅でレイアウトされるようにしてみましょう。
GridはGridRowと組み合わせてレイアウトを行います。先ほどのサンプルコードからは、VStackをGridにしてHStackをGridRowにするだけで変更は完了します。
var body: some View { // VStack → Grid Grid { row(name: "鮭", progress: 0.5, quantity: 25) row(name: "昆布", progress: 0.2, quantity: 9) row(name: "明太子", progress: 0.3, quantity: 16) } } func row(name: String, progress: CGFloat, quantity: Int) -> some View { // HStack → GridRow GridRow { Text(name) .gridColumnAlignment(.leading) ProgressView(value: progress) Text("\(quantity)") .gridColumnAlignment(.trailing) } }
これを実行すると、簡単に意図した通りのレイアウトを構築することができます。
gridCellColumnsを使うと全体のカラム数に対して何個分の領域を確保するかを指定することも可能なので、とても柔軟なレイアウト処理を行うことが可能です。
Layoutによるカスタムレイアウト作成
Gridを使うことでより便利なレイアウトを組むことが可能になりましたが、さらに柔軟なレイアウトを実現したい場合はカスタムレイアウトの定義も可能です。
具体的には、Layoutプロトコルに準拠したクラスを定義してカスタムのレイアウトを宣言します。実装が必要となるメソッドは、コンテナの大きさを決定づけるsizeThatFitsと、サブビューの表示位置を指定するplaceSubviewsの2つとなっています。
struct CustomLayout: Layout { // レイアウトするコンテナの大きさを計算する func sizeThatFits( proposal: ProposedViewSize, // コンテナからのサイズ提案 subviews: Subviews, // サイズを提案するサブビューへの参照(直接アクセスすることはできない) cache: inout () // 計算結果のキャッシュ ) -> CGSize { } // サブビューの表示位置を指定する func placeSubviews( in bounds: CGRect, // サブビューを配置する必要のある領域 proposal: ProposedViewSize, subviews: Subviews, cache: inout () ) { } }
詳細な実装についてはWWDCの動画にて詳しく解説されているので割愛しますが、下記のような形でsubviewから最大サイズを取得したり、
let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) } let maxSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in CGSize( width: max(currentMax.width, subviewSize.width), height: max(currentMax.height, subviewSize.height) ) }
計算したサイズを元にsubviewの位置を決定づけたりすることが可能になります。
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () ) { let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let sizeProposal = ProposedViewSize(maxSize) var x = bounds.minX + maxSize.width / 2 for index in subviews.indices { subviews[index].place( at: CGPoint(x: x, y: bounds.midY), anchor: .center, proposal: sizeProposal ) x += maxSize.width + spacing[index] } }
LayoutValueKeyを用いると、カスタムレイアウト内からレイアウト対象のViewに紐づく個別データへのアクセスも可能になるので、複雑なレイアウトを実現したい場合は活用できるかと思います。
ViewThatFitsによるコンテキストに応じたレイアウトの自動選択
ViewThatFitsに複数のViewを与えると、コンテナのサイズとサブビューの最大サイズに応じて、自動でレイアウトスペースに収まる最適なViewを選択させることが可能になります。
例えば下記のような実装をした場合を見てみます。ViewThatFits内に「Textを水平方向に並べたHStack」と「垂直方向に並べたVStack」を両方定義しています。
ViewThatFits { HStack { Text("これはとても長い文章です1") Text("これはとても長い文章です2") Text("これはとても長い文章です3") } VStack { Text("これはとても長い文章です1") Text("これはとても長い文章です2") Text("これはとても長い文章です3") } }
これを実行した場合、サブビューがコンテナのサイズに収まるレイアウト(HStack or VStack)が自動で選ばれることになります。添付画像のように、Portraitモードでコンテナの横幅が短い時はVStackレイアウトが選択され、Landscapeモードで横幅が長い場合はHStackレイアウトが選択されています。
AnyLayoutを用いた動的なレイアウト変更
AnyLayoutを用いることで、レイアウトを動的に変更することも簡単になりました。下記のサンプルコードはトグルの状態に応じて水平方向と垂直方向のレイアウトを動的に変更できるようにした実装です。iOS 15以前のAPIを駆使すると冗長な記述が必要になってしまいますが、AnyLayoutを使うことでレイアウトクラスを保持する変数に型消去を用いることが可能になります。これにより、状態に応じて柔軟にレイアウトを切り替える実装がしやすくなりました。
var body: some View { let layout = isOn ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout()) VStack { Toggle("レイアウトを変更", isOn: $isOn.animation()) layout { Text("これはとても長い文章です1") Text("これはとても長い文章です2") Text("これはとても長い文章です3") } } }
SFSymbolsの便利な新機能
Variable symbols
SFSymbolsにも便利なアップデートが入り、Variable symbolsという機能が追加されました。可変値に対応したSFSymbolsイメージに対して0~1のvariableValueを受け渡すことで、OSがコンテキストに応じた表示に自動で切り替えてくれるようになっています。
Image(systemName: "wifi", variableValue: 0.0) Image(systemName: "wifi", variableValue: 0.3) Image(systemName: "wifi", variableValue: 0.5) Image(systemName: "wifi", variableValue: 1.0)
表現力の向上
Colorの新しいgradientプロパティや、ShapeStyleの新しいshadow修飾子を活用するとアイコンをよりリッチな表示にすることも可能です。
まとめ
以上が、WWDC2022で発表されたSwiftUIに関する主要なトピックでした。本編ではより細かなアップデートや実装方法の詳細も紹介されておりますので、さらに深くキャッチアップしたい方は下記セッション等をご覧いただくとより理解が深まるかと思います。
おわりに
今回の記事では、WWDC2022で発表されたSwiftUI関連のアップデートについてをご紹介しました。今までは細かいユースケースに対応しきれないことも多かったSwiftUIですが、これらの進化を通じてより柔軟性を高めています。今後のSwiftUIを活用したアプリ開発は、より素早く簡潔に実装していくことができそうです。次回は新たに登場した図形描画フレームワークであるChartsと、UIKit関連の進化についてご紹介いたします。