MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

SwiftUIでリサイズ可能なシートを利用してみよう


タクシーアプリ『GO』の iOS アプリを開発している黒田です。

今回はiOS16で追加されたSwiftUIのsheet用の修飾子について調べたので紹介します。また、最後は『GO』での適用について考えてみました。


はじめに

iOS16からSwiftUIのsheet(シート)がリサイズ可能になりました。

iOS15以前は、UIKitで実装したものをUIViewControllerRepresentable でラップし SwiftUI で扱うようにする必要がありましたが、iOS16で追加された修飾子により、

今回は、SwiftUIで追加されたsheetの修飾子について使い方を紹介します。

シートをリサイズ可能にする

iOS16からpresentationDetentsという修飾子が追加されました。

定義は以下のとおりです。

func presentationDetents(_ detents: Set<PresentationDetent>) -> some View

引数のdetentsはsheetでサポートされるdetent(戻り止め)のsetです。

複数のdetentを指定すると、ユーザーはシートをドラッグしてサイズを変更することができます。

下記の例だと、.medium.largeが指定されているので、シートは画面の半分のサイズと全画面に変更できます。

struct ContentView: View {
  @State var showSheet = false
  
  var body: some View {
    NavigationView {
      Button("Sheet") {
        showSheet.toggle()
      }.sheet(isPresented: $showSheet, onDismiss: {
        print("Sheet dismissed")
      }) {
        ZStack {
          Color.red
          Text("Hello, Swift")
        }
        .ignoresSafeArea()
        .presentationDetents([.medium, .large])
      }
    }
  }
}

An image from Notion

detentの指定

detentに指定できるものには以下が用意されています。

An image from Notion

.height.fractionについてはそれぞれ以下のように指定します。

.presentationDetents([
  .height(300),  // 高さが300
  .fraction(0.3) // 画面全体の30% 
])

.customはCustomPresentationDetentプロトコルに準拠した構造体を定義し、指定します。

以下に例を示します。

struct BarDetent: CustomPresentationDetent {
    static func height(in context: Context) -> CGFloat? {
        max(44, context.maxDetentValue * 0.1)
    }
}

extension PresentationDetent {
    static let bar = Self.custom(BarDetent.self)
}
.presentationDetents([.bar])

ドラッグインジケーターの表示

presentationDragIndicator修飾子でシート上部にドラッグインジケーターを表示するかどうかを制御できます。

なお、detentを複数指定した場合、デフォルトでドラッグインジケーターが表示されます。

.presentationDragIndicator(.visible)
An image from Notion

iOS16.4で追加された修飾子

iOS16.4で以下のような修飾子が追加されました。

シートのスワイプジェスチャーの動作設定

presentationContentInteraction修飾子でシートを上下にスワイプした際の動作を指定できます。

func presentationContentInteraction(PresentationContentInteraction) -> some View

デフォルトではシートの中がスクロール可能なViewの場合にスワイプアップするとシートの高さが変わり、シートが最大サイズに達した場合にのみViewがスクロールされます。

.scrollsを指定した場合、ScrollViewのスクロールが優先されます。シートサイズの変更はドラッグインジケーターを利用します。

An image from Notion

.resizesを指定した場合、シートのリサイズが優先されます。presentationDetentsで指定したdetentsの中で最大サイズの場合のみViewのスクロールが可能になります。

(デフォルトの挙動と同じになります)

An image from Notion

背後にあるViewとのインタラクション

シートの背後にあるViewを操作できるかどうかの制御

presentationBackgroundInteraction修飾子を使用します。

func presentationBackgroundInteraction(_ interaction: PresentationBackgroundInteraction) -> some View

引数のinteractionは、シートの背後にあるViewを操作できるかどうかを指定します。

An image from Notion

下記の例では、高さが.height(120)以下の場合にシート背後のViewの操作が可能になります。

.presentationBackgroundInteraction(.enabled(upThrough: .height(120))

ジェスチャーによるdismissを条件付きで防ぐ

ユーザーはジェスチャー(シートを画面の一番下までスワイプダウンしたり、背面のViewをタップしたり)を使用してシートを閉じることができます。

これをinteractiveDismissDisabled修飾子を使用して条件付きで防ぐことができます。

  • isDisabled == trueの時は、シートがdetentsの最小の高さまで小さくなるが、閉じることはできません
  • isDisabled == trueかつpresentationBackgroundInteractionの引数に.enabled(upThrough:))を指定した場合、背面タップすると指定した高さまで下がります
    • (スワイプダウンや背面タップすると、presentationDetentsで指定したdetentの最小の高さまで下がりますが、シートを閉じることはできません。)

シート上部の角丸サイズを変更

presentationCornerRadius修飾子を使用して、シート上部の角の半径を指定できます。

.presentationDragIndicator(100)

上記のように指定すると表示は以下のようになります。

An image from Notion

シートの背景

シートの背景にスタイルを適用できます。

presentationBackground(_:)修飾子もしくはpresentationBackground(alignment:content:)修飾子を使用します。

presentationBackground(_:)

func presentationBackground<S>(_ style: S) -> some View where S : ShapeStyle

下記の例では.thinMaterialを指定し、すりガラス表示にして背景を透過しています。

Button("Sheet") {
    showSheet.toggle()
  }.sheet(isPresented: $showSheet, onDismiss: {
    print("Sheet dismissed")
  }) {
    Text("Hello, Swift")
    .presentationDetents([.medium, .large])
    .presentationBackground(.thinMaterial)
  }
An image from Notion

グラデーションも指定できます。

.presentationBackground(
  LinearGradient(
    colors: [.purple, .pink, .orange, .yellow],
    startPoint: .topTrailing,
    endPoint: .bottomLeading
  )
)
An image from Notion

presentationBackground(alignment:content:)

func presentationBackground<V>(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View where V : View

シートの背景色を変える場合は、上記の修飾子を使用し以下のようにします。

.presentationBackground { Color.blue.opacity(0.5 }
An image from Notion

以上、SwiftUIで追加されたsheetに関する修飾子を紹介しました。

タクシーアプリ『GO』におけるシート表示

上記の修飾子を利用し、『GO』にSwiftUIのsheetを適用してみる場合を考えてみます。

『GO』では配車確定後、到着前、到着後、乗車中画面においてサイズ変更可能なシートを表示しています。

それぞれの画面において、以下の画像のようにシートを小さく表示しマップを操作できる表示モードシートを画面上部まで広げて表示するモードの2つのモードをスワイプで切り替えられます。

An image from Notion

また、プレミアム車両やGO Reserve車両が配車された場合、小さい方の表示モードのシートサイズが変わります。

An image from Notion

これらを上記で紹介した修飾子を利用し、どのように実装すればよいか考えてみます。

まず、各画面のサイズをCustomPresentationDetentを利用し、定義します。(値は適当です)

struct MinimalDetent: CustomPresentationDetent {
    static func height(in context: Context) -> CGFloat? {
        switch 車両タイプ {
            case .isNormalTaxi:
								switch 画面タイプ {
										case 配車確定後, 到着前:
												return .fraction(0.3)
										case 到着後:
												return .fraction(0.25)
										 ...
								
				    case .isPremiumTaxi:
								return .fraction(0.4)
            case .isReserveTaxi:

                ...

				}
    }
}

struct MaximalDetent: CustomPresentationDetent {
    static func height(in context: Context) -> CGFloat? {
        .fraction(0.9)
    }
}

extension PresentationDetent {
    static let minimal = Self.custom(BarDetent.self)
    static let maximal = Self.custom(BarDetent.self)
}

そしてpresentationDetentsの引数に定義します。

.sheet(isPresented: $showSheet, onDismiss: {
    //
}) {
    XXXView // 各画面のView
    .presentationDetents([.minimal, .maximal])
} 

シート内はスクロール可能ですが、リサイズを優先するため、presentationContentInteraction修飾子はデフォルトで良いため指定は不要です。

背後にあるViewはシートが小さい場合に背後のMapを操作可能であり、シートが大きい場合は操作不可になっています。そのため、presentationBackgroundInteraction修飾子を使用し、引数は.enabled(upThrough:)にします。

.sheet(isPresented: $showSheet, onDismiss: {
    //
}) {
    XXXView // 各画面のView
    .presentationDetents([.minimal, .maximal])
		.presentationBackgroundInteraction(.enabled(upThrough: .minimal))
}

シート上部の角丸はデフォルトより少し丸いので、presentationDragIndicator修飾子を使用します。

.sheet(isPresented: $showSheet, onDismiss: {
    //
}) {
    XXXView // 各画面のView
    .presentationDetents([.minimal, .maximal])
		.presentationBackgroundInteraction(.enabled(upThrough: .minimal))
		.presentationDragIndicator(20)
}

これでSwiftUIでの画面実装が実現できそうです。

今回は時間の都合上実際のアプリへの組み込みを試すことはできませんでしたが、今後時間を見つけて試してみたいと思います。

現状(2023年5月時点)ではライブラリを使用して実現していますが、今後OSの最低バージョンが16.4以上になった場合は今回紹介した修飾子を利用しSwiftUIの実装に切り替えたいと思います。

(その時には別の新しい技術が登場しているかもしれませんが)

まとめ

今回はiOS16で追加されたSwiftUIでのリサイズ可能なシートの使い方およびiOS16.4で追加された修飾子について調べました。

『GO』ではまだまだUIKitを使用している部分が多数ありますが、徐々にSwiftUIへ移行していっています。今後のSwiftUIの進化に期待し、移行作業を進めつつ新しい技術のキャッチアップを継続していきたいと思います。


We're Hiring!

📢
GO株式会社ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @goinc_techtalk のフォローもよろしくお願いします!