フューチャー技術ブログ

SwiftUIのカスタムアラートダイアログについて考える

はじめに

こんにちは。HealthCare Innovation Group(HIG)1所属の清水です。

本記事は、夏の自由研究ブログ連載2023の5本目です。

SwiftUIにおけるアラートダイアログを自作で実装する場合、どんな方法があるか考えてみました。

Swiftを用いたiOSアプリ開発で、アラートを自作したいと考えたことはありませんか? Webアプリでもモバイルアプリでも、ユーザに問題を報告したり動作による影響を警告したりする上で、アラートは重要です。

SwiftUIでもalertが標準で用意されています。ただし、フォントサイズやダイアログのサイズが変更できないため、iPadで利用する場合画面に対してかなり小さく扱いづらいです。

SwiftUI標準のalertをシミュレータ iPad Pro (12.9-inch) で表示した際の図

画面に対してかなり小さい・・・。

そこで、「自由にカスタマイズできるアラートダイアログを自作する場合、どのような実装方法があるか」を自由研究の題材とすることにしました。

今回試してみた3種類のカスタムアラートを、実装方法と共に紹介したいと思います。
※もっと良い実装方法をご存知でしたら、教えていただきたいです!

本記事では触れていませんが、カスタムアラートダイアログに関するライブラリも公開されています。合わせて確認頂けると良いと思います(例: CustomAlert

検証内容

アラートは、通常の画面より手前(通常画面に重なる形)に表示します。
今回は、重ねて表示するためのコンポーネントの中から、以下の3種類の実装方法で比較していきたいと思います。

  1. fullScreenCoverを利用した方法
  2. overlayを利用した方法
  3. ZStackを利用した方法

事前準備

画面に重ねて表示するためのダイアログをViewとして用意しています。SwiftUI標準のalertに見た目を寄せたアラートViewを、iPad Pro (12.9-inch) 向けにサイズ調整したViewがこちらです。このViewをいくつかの方法で重ねていきます。

※実装は、記事後半に記載しています。

1. fullScreenCoverを利用した方法

1つ目は、fullScreenCoverを利用した方法です。

引数に渡したフラグがtrueの時、全画面を覆うViewをモーダル表示します。

他の方法と比較

  • メリット
    • 画面全体にモーダル表示するため、アラート表示中に他の動作ができないように制限しやすい。
  • デメリット
    • fullScreenCover は表示時に画面下から上まで覆うアニメーションを伴って表示するため、アニメーションのカスタマイズが難しい(調べた限りほぼできない)。
      アニメーションを非活性にすることはできる。2

2. overlayを利用した方法

2つ目は、overlayを利用した方法です。
名前の通り、親Viewより手前に指定したViewを重ねて表示します。アラートの使用用途を考えるとちょうど良さそうです。

他の方法と比較

  • メリット
    • overlay自体は重ねて表示するだけなので、アニメーションなどをカスタマイズしやすい。
  • デメリット
    • あくまでも元となるViewに重ねるため、重ねて表示する範囲は呼び出すViewに依存する。
      例えば、VStackに紐づけるとVStackの範囲が重ねるViewの表示範囲となり、表示範囲外は操作できてしまう(下のgif参照)

3. ZStackを利用した方法

3つ目は、ZStackを利用した方法です。
fullScreenCoveroverlayはViewのInstance Methodを利用していたのですが、こちらは内包するViewの表示順を制御するものなので、毛色が異なります。

他の方法と比較

  • メリット
    • アプリ大元のViewに重ねることで、画面全体を覆って表示制御できる。
  • デメリット
    • ダイアログ表示を制御するフラグやダイアログに表示する内容をApp階層まで伝える必要があるため、単一のView内で状態管理が完結しない。

コード実装例

今回利用したソースコードは、こちらです。

App

struct IsShowAlert: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}

extension EnvironmentValues {
var isShowAlert: Binding<Bool> {
get { self[IsShowAlert.self] }
set { self[IsShowAlert.self] = newValue }
}
}

@main
struct SampleApp: App {
@State private var isShowAlert: Bool = false

var body: some Scene {
WindowGroup {
ZStack {
ContentView()
.environment(\.isShowAlert, $isShowAlert)
/// 3. `ZStack`を利用した実装方法
if isShowAlert {
CustomAlertView(alertTitle: Text("タイトル")) {
Button("OK") {
$isShowAlert.wrappedValue.toggle()
}
} message: {
Text("メッセージ")
}
}
}
}
}
}

ContentView

struct ContentView: View {
/// 標準アラートの表示を管理するフラグ
@State var isShowDefaultAlert: Bool = false
/// `fullScreenCover`を利用したカスタムアラートの表示を管理するフラグ
@State var isShowFullScreenCoverAlert: Bool = false
/// `overlay`を利用したカスタムアラートの表示を管理するフラグ
@State var isShowOverlayAlert: Bool = false
/// `ZStack`を利用したカスタムアラートの表示を管理するフラグ
@Environment(\.isShowAlert) var isShowAlert

var body: some View {
ZStack {
VStack(spacing: 50) {
Button(
action: {
isShowDefaultAlert = true
},
label: {
Text("標準アラートを表示する")
}
)
Button(
action: {
isShowFullScreenCoverAlert = true
},
label: {
Text("`fullScreenCover`のアラートを表示する")
}
)
Button(
action: {
isShowOverlayAlert = true
},
label: {
Text("`overlay`のアラートを表示する")
}
)
Button(
action: {
isShowAlert.wrappedValue = true
},
label: {
Text("`ZStack`のアラートを表示する")
}
)
}
}
.alert(Text("タイトル"), isPresented: $isShowDefaultAlert) {
Button("OK") {
isShowDefaultAlert.toggle()
}
} message: {
Text("メッセージ")
}
.alertFullScreenCover(Text("タイトル"), isPresented: $isShowFullScreenCoverAlert) {
Button("OK") {
isShowFullScreenCoverAlert.toggle()
}
} message: {
Text("メッセージ")
}
.alertOverlay(Text("タイトル"), isPresented: $isShowOverlayAlert) {
Button("OK") {
isShowOverlayAlert.toggle()
}
} message: {
Text("メッセージ")
}
}
}

Extensions

extension View {
/// 1. `fullScreenCover`を利用した実装方法
func alertFullScreenCover<A, M>(
_ title: Text,
isPresented: Binding<Bool>,
@ViewBuilder actions: @escaping () -> A,
@ViewBuilder message: @escaping () -> M
) -> some View where A : View, M : View {
fullScreenCover(isPresented: isPresented) {
CustomAlertView(alertTitle: title, actions: actions, message: message)
}
}

/// 2. `overlay`を利用した実装方法
func alertOverlay<A, M>(
_ title: Text,
isPresented: Binding<Bool>,
@ViewBuilder actions: @escaping () -> A,
@ViewBuilder message: @escaping () -> M
) -> some View where A : View, M : View {
overlay {
if isPresented.wrappedValue {
CustomAlertView(alertTitle: title, actions: actions, message: message)
}
}
}
}

CustomAlertView

import SwiftUI

struct CustomAlertView<A, M>: View where A : View, M : View {

var alertTitle: Text
@ViewBuilder var actions: () -> A
@ViewBuilder var message: () -> M

var body: some View {
ZStack {
// 背景部分
Color.black
.opacity(0.2)
.edgesIgnoringSafeArea(.all)
// ダイアログ部分
VStack {
alertTitle
.font(.largeTitle)
.bold()
.padding(.top, 20)
.padding(.bottom, 10)
message()
.font(.title)
Divider()
.padding(.top, 20)
actions()
.font(.title)
.padding(.vertical, 20)

}
.frame(width: UIScreen.main.bounds.width / 2)
.background(.white)
.cornerRadius(15)
}
}
}

struct CustomAlert_Previews: PreviewProvider {
static var previews: some View {
CustomAlertView(alertTitle: Text("タイトル"), actions: {
Button("OK") {}
}, message: {
Text("メッセージ")
})
}
}

さいごに

本記事では、自由研究としてアラートダイアログを自作する際の実装方法に関して、3種類を比較してみました。

アラートダイアログは性質上、表示中他の操作ができないことが求められると思います。今回試した中では、3つ目のZStackを利用した方法がアプリ大元のViewに重ねることでアラートを一番手前に表示する要件を満たしやすいと感じました。

また、今回試して分かったメリット・デメリットを以下の表にまとめました。

試した実装方法 メリット デメリット
1. fullScreenCoverを利用した方法 画面全体にモーダル表示するため、アラート表示中に他の動作ができないように制限しやすい。 fullScreenCover は表示時に画面下から上まで覆うアニメーションを伴って表示するため、アニメーションのカスタマイズが難しい(調べた限りほぼできない)。
2. overlayを利用した方法 overlay自体は重ねて表示するだけなので、アニメーションなどをカスタマイズしやすい。 あくまでも元となるViewに重ねるため、重ねて表示する範囲は呼び出すViewに依存する。
3. ZStackを利用した方法 アプリ大元のViewに重ねることで、画面全体を覆って表示制御できる。 ダイアログ表示を制御するフラグやダイアログに表示する内容をApp階層まで伝える必要があるため、単一のView内で状態管理が完結しない。

何かしらの参考になれば幸いです。

参考リンク

https://developer.apple.com/documentation/SwiftUI/View/alert(_:isPresented:actions:)-1bkka

https://developer.apple.com/documentation/swiftui/view/fullscreencover(ispresented:ondismiss:content:)

https://developer.apple.com/documentation/swiftui/view/overlay(alignment:content:)


  1. 1.医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。
  2. 2.SwiftUI: fullScreenCover with no animation?