アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

もう画面下部からは出現しない!? iOS 26で変わるアクションシート

こんにちは、開発部第1グループの西岡です。

iOS 26 がリリースされて、いわゆる「Liquid Glass」と呼ばれる新しいデザインシステムの導入により、UI/UXが大きく変わりつつあります。中でもユーザーの意思決定を促す「アクションシート」は、従来のボトムアップ型から、発生源付近に浮かぶ『フローティング・バブル』形式へと変わりました。

本記事では、Xcode 26 を用いて、iOS 18 と iOS 26 での挙動の違いや実装方法、そしてデザイナーやエンジニアの関心事である「表示位置の制御」や「後方OSバージョンとの互換性」について検証してみました。

確認環境

今回の検証は以下の環境で行っています。

  • Xcode: 26.1
  • iOS: 18.2 (Before) / 26.1 (After)
  • Swift: 6.1

アクションシートとは? 何が変わったのか?

アクションシートとは、特定の操作に対する複数の選択肢を提示したり、実行の最終確認を行ったりするためのモーダルビューです。 iOS 18 までは画面下部からスライドアップする形式が標準でした。

画面下部からスライドアップする従来のアクションシート

ところが、iOS 26 からはアクションシートが様変わりします。 (※UIDesignRequiresCompatibilityキーは未設定です)

ボタン付近に表示される新アクションシート
従来のアクションシートとは異なり、アクションの発生源(アンカー)付近に浮かび、その吹き出しがボタンを差し示しています。

実装方法

次に Liquid Glass 時代のアクションシートの実装をみていきましょう。

SwiftUI:confirmationDialog

アクションシートをSwiftUIで実装する場合、confirmationDialog がその役割を担います。

SwiftUI でアクションシートを表示する(iOS 26)

実装例

上記のサンプルコードはこちらです。

@State var isShowingDialog: Bool = false

// (1) アクションの発生源
Button("アクションシート(ConfirmationDialog)") {
    isShowingDialog = true
}
// (2) ボタンに対してconfirmationDialogを設定
.confirmationDialog(
    "タイトルです。", 
    isPresented: $isShowingDialog,
    titleVisibility: .visible,
    actions: {
        if #available(iOS 26.0, *) {
            Button("確定", role: .confirm) {
                // Handle option 1
                print("Confirm action selected")
            }
            Button("閉じる", role: .close) {
                // Handle option 1
                print("Close action selected")
            }
        }
        // (3) 「キャンセル」ボタン
        // Liquid Glass(iOS26〜)ではシート上に現れない
        Button("キャンセル", role: .cancel) {
            // アクション:欄外タップでシートが消滅した際に、ここが呼ばれます。
            print("Cancel action selected")
        }
        Button("破壊的な選択", role: .destructive) {
            // Handle destructive action
            print("Destructive action selected")
        }
        Button("デフォルト") {
            // Handle destructive action
            print("Default action selected")
        }
    },
    message: {
        Text("メッセージ内容です。")
    }
)

一番のポイントは、アクションの発生源(アンカー)の近くに配置することです。基本的には「modifierを付与したView」が自動的にアクションシートの発生源(アンカー)となるためです。

上記のソースコードでは、(1) アクションを発生させるUI要素(ボタン等)を配置したら、このUI要素(ボタン等)に (2) confirmationDialog を配置しています。こうすることでアクションシートは自動的にそのUI要素付近に浮遊し、吹き出し方向が発生源に向かいます。 今まではconfirmationDialogの配置先とシート表示はある意味で無関係でしたが、iOS 26 になるとアクションの発生源も意識する必要がありそうです。

また、 iOS 26 からは (3) ボタンロール.cancel の挙動も少し変わります。従来のアクションシートでは.cancel を設定するとシート上に「キャンセル/Cancel」ボタンが表示されましたが、iOS 26(Liquid Glass)になるとここが省略されます。「キャンセル」に相当する操作を行うには、シート欄外をタップするとアクションシート消滅し、その直後にButton内のコールバックが呼ばれます。

※ なおSwiftUI初期に提供されていた ActionSheet型 は既に非推奨になっており、iOS 26 では一部表示崩れが発生するという報告もあるようです。ご注意下さい!

参考: iOS 26でSwiftUIのactionSheetが表示崩れする問題をconfirmationDialogで解決した | DevelopersIO

吹き出しの位置はどこまで指定できるのか?

SwiftUIでは、基本的に「modifierを付与したView」が自動的に発生源(アンカー)となるため、アクションシートの浮遊位置やアンカー位置の調整など、現時点の検証では「位置指定の自由度」がさほど高くないようです。ただし、いくつか工夫することは可能です。

その1. アンカー(Source)の動的制御: 特定のボタンではなく、リストの行や特定のアイコンからアクションシートを出したい場合などが挙げられます。ただし、.confirmationDialog を付与する場所を慎重に選ぶ必要はあります。

struct SourceSelectionView: View {
    @State private var selectedItem: String?
    
    var body: some View {
        List(["Item A", "Item B", "Item C"], id: \.self) { item in
            HStack {
                Text(item)
                Spacer()
                Button {
                    selectedItem = item
                } label: {
                    Image(systemName: "ellipsis.circle")
                }
                // 各行のボタンに直接付与することで、そのボタンがアンカーになる
                .confirmationDialog(
                    "\(item)のアクション",
                    isPresented: Binding(
                        get: { selectedItem == item },
                        set: { if !$0 { selectedItem = nil } }
                    ),
                    titleVisibility: .visible
                ) {
                    Button("編集") {
                        print("Default action selected")
                    }
                    Button("削除", role: .destructive) {
                        print("Destructive action selected")
                    }
                }
            }
        }
    }
}

その2. .popover を使って厳密な指定: .confirmationDialog の代わりに.popoverで実装して、シートレイアウトを含めて厳密な指定や調整を行う代替案です。
・アンカー指定: attachmentAnchor 引数に対して .rect(.bounds).point(UnitPoint.bottom) を指定することで、ビューのどの位置から吹き出しを出すかを調整します。
・矢印の向き: arrowEdge 引数を指定することで、ソースの上下左右どちらに展開するかを制御します。

その3. UIKitの UIAlertController で実装する。(詳しくは後述へ)

UIKitでの実装 - UIAlertController

アクションシートの挙動も変わった!?

さて、UIKitの元祖アクションシートも未だ健在ですが、実はUIKitのアクションシートを制御する UIAlertController も、iOS 26でその挙動が大きく変わりました。

従来のアクションシートは、こんな感じの実装でした。

// (1) 
let alert = UIAlertController(
    title: "タイトルです。",
    message: "メッセージ内容です。",
    preferredStyle: .actionSheet
)
// (2)
alert.addAction(UIAlertAction(title: "破壊的な選択", style: .destructive))
alert.addAction(UIAlertAction(title: "デフォルト選択", style: .default))
alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel))
// (3)
present(alert, animated: true)

一連の実装では、まず (1) アクションシート(.actionSheet)を指定して UIAlertController インスタンスを生成し、(2) そのインスタンスに複数の選択ボタン(UIAlertAction) を追加します。最後に (3) present(:) メソッドを呼び出す、という流れでした。

iOS 18.2 で動かした時のアクションシートはこちらです。

iOS 18.2 での挙動

これを Xcode 26.1 でビルドし、iOS 26.1 で実行するとこんな挙動となります。

アクションシート(iOS 26.1)アラート(iOS 26.1)
iOS 26.1 で実行した結果(左: アクションシート、右: アラート)

なんと… アラートが表示されてしまいました。実際のアラート(右図)と見比べてもアラートです。 先ほどのSwiftUI と同じような 新時代のアクションシート を期待していたのですが…。 UIKitの UIAlertController はどうなっているのでしょうか?

iOS 26 からの追加実装

実はAppleのドキュメントにヒントがあります。

Getting the user’s attention with alerts and action sheets | Apple Developer Documentation

実は preferredStyle: .actionSheet を指定した場合、 popoverPresentationController によるアンカー指定が実質的に必要となりました。

// (1) 
let alert = UIAlertController(
    title: "タイトルです。",
    message: "メッセージ内容です。",
    preferredStyle: .actionSheet
)
// (2-1)
alert.addAction(UIAlertAction(title: "破壊的な選択", style: .destructive))
alert.addAction(UIAlertAction(title: "デフォルト選択", style: .default))
alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel))

// (2-2) iOS 26ではこの設定が不可欠
if let popover = alert.popoverPresentationController {
    // 発生源となるUIButtonなどを指定
    popover.sourceView = sender 
    // sender.frameではなく、sender.boundsを指定することで
    // ボタン自体の領域を基準とした適切な位置に吹き出しが出る
    popover.sourceRect = sender.bounds
    // 吹き出しが出る方向を上下に限定したい場合
    popover.permittedArrowDirections = [.up, .down, .left, .right] 
}

// (3)
present(alert, animated: true)

このアンカー指定 (2-2) を怠ると、画面中央に不自然に浮いてしまい、結果的に意図に反したアラート表示となってしまうようです。

ちなみに popoverPresentationController 自体は iOS 26 リリース以前(iOS 8 〜)から存在しているインターフェースです。OSバージョンごとに分岐処理が必要か懸念されますが、iOS 18 以下で検証してみたところ、iOS 26 未満では alert.popoverPresentationControllernil を返します。そのため、既存のコードに対しても後方互換性が保たれていることが確認できました。

アンカーを指定して再度実行した結果がこちらです。

UIKitでのアクションシート表示に成功
今度こそ新しいアクションシートが表示されました。

popoverPresentationControllerで何が指定できるのか?

API 仕様
sourceView sourceRect で指定する座標の「親」となるビューを指定します。通常、ユーザーがタップしたボタンや、詳細を表示したい対象のセルなどを指定します。
sourceRect sourceView の座標系に基づいた矩形を指定します。
permittedArrowDirections UIPopoverArrowDirection 型(OptionSet)で指定します。
.any: システムが最適な方向を自動選択します(デフォルト)。
.up, .down, .left, .right: 特定の方向を固定または組み合わせて指定します。

まとめ

iOS 26の変更点を整理すると、以下のようになります。

  • iOS 18 から iOS 26 にかけて、アクションシートのデザインやUI/UXが大きく変わる。
  • SwiftUI でアクションシートを実装する場合、アクションの発生源となるUI要素(ボタン)にconfirmationDialog を配置する必要がある。
  • SwiftUI のアクションシートの浮遊位置やアンカーは、システムが自動調整してくれる。
  • UIKit でアクションシートを実装する場合、popoverPresentationController に対してアンカー指定の追加が必須となる。もし未設定のままだとアラート形式で表示される恐れがある。
  • UIKit のアクションシートでは、アンカー属性(sourceView, sourceRect, permittedArrowDirections)を調整することができる。

さいごに

iOS 26では、これまで以上に「どのUI要素から発生したのか」をコード上で明示する必要性を感じました。特に既存アプリのアップデートの時には、アクションシートが意図しない位置に表示されないよう、アンカー設定も再点検が必要になりそうです。アクションシート一つの変化をとっても、より直感的で物理的な実体に近いUIを目指すAppleの姿勢が色濃く出ている気がしました。

References