タクシーアプリ『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])
}
}
}
}
detentに指定できるものには以下が用意されています。
.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)
iOS16.4で以下のような修飾子が追加されました。
presentationContentInteraction修飾子でシートを上下にスワイプした際の動作を指定できます。
func presentationContentInteraction(PresentationContentInteraction) -> some View
デフォルトではシートの中がスクロール可能なViewの場合にスワイプアップするとシートの高さが変わり、シートが最大サイズに達した場合にのみViewがスクロールされます。
.scrollsを指定した場合、ScrollViewのスクロールが優先されます。シートサイズの変更はドラッグインジケーターを利用します。
.resizesを指定した場合、シートのリサイズが優先されます。presentationDetentsで指定したdetentsの中で最大サイズの場合のみViewのスクロールが可能になります。
(デフォルトの挙動と同じになります)
presentationBackgroundInteraction修飾子を使用します。
func presentationBackgroundInteraction(_ interaction: PresentationBackgroundInteraction) -> some View
引数のinteractionは、シートの背後にあるViewを操作できるかどうかを指定します。
下記の例では、高さが.height(120)以下の場合にシート背後のViewの操作が可能になります。
.presentationBackgroundInteraction(.enabled(upThrough: .height(120))
ユーザーはジェスチャー(シートを画面の一番下までスワイプダウンしたり、背面のViewをタップしたり)を使用してシートを閉じることができます。
これをinteractiveDismissDisabled修飾子を使用して条件付きで防ぐことができます。
presentationCornerRadius修飾子を使用して、シート上部の角の半径を指定できます。
.presentationDragIndicator(100)
上記のように指定すると表示は以下のようになります。
シートの背景にスタイルを適用できます。
presentationBackground(_:)修飾子もしくはpresentationBackground(alignment:content:)修飾子を使用します。
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)
}
グラデーションも指定できます。
.presentationBackground(
LinearGradient(
colors: [.purple, .pink, .orange, .yellow],
startPoint: .topTrailing,
endPoint: .bottomLeading
)
)
func presentationBackground<V>(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View where V : View
シートの背景色を変える場合は、上記の修飾子を使用し以下のようにします。
.presentationBackground { Color.blue.opacity(0.5 }
以上、SwiftUIで追加されたsheetに関する修飾子を紹介しました。
上記の修飾子を利用し、『GO』にSwiftUIのsheetを適用してみる場合を考えてみます。
『GO』では配車確定後、到着前、到着後、乗車中画面においてサイズ変更可能なシートを表示しています。
それぞれの画面において、以下の画像のようにシートを小さく表示しマップを操作できる表示モードとシートを画面上部まで広げて表示するモードの2つのモードをスワイプで切り替えられます。
また、プレミアム車両やGO Reserve車両が配車された場合、小さい方の表示モードのシートサイズが変わります。
これらを上記で紹介した修飾子を利用し、どのように実装すればよいか考えてみます。
まず、各画面のサイズを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の進化に期待し、移行作業を進めつつ新しい技術のキャッチアップを継続していきたいと思います。
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @goinc_techtalk のフォローもよろしくお願いします!