SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

iOSエンジニアたちと振り返るWWDC

【WWDC2022総復習】「機能不足」は過去の話、さらに表現力を増したSwiftUI

  • X ポスト
  • このエントリーをはてなブックマークに追加

新機能の追加で、柔軟なレイアウト実装が可能に

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関連の進化についてご紹介いたします。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
iOSエンジニアたちと振り返るWWDC連載記事一覧

もっと読む

この記事の著者

横山 拓也(STORES 株式会社)(ヨコヤマ タクヤ)

 STORES 株式会社テクノロジー部門モバイル本部にて、STORES レジ のiOSアプリ開発に従事。 水産学部出身でエンジニアになり、主にモバイルアプリ開発を主軸として経験を積み、現在ではAndroidやFlutterなども書く。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/17313 2023/03/10 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング