Fixed an issue with SwiftUI actionSheet display glitches in iOS 26 by using confirmationDialog instead

Fixed an issue with SwiftUI actionSheet display glitches in iOS 26 by using confirmationDialog instead

2025.09.17

This page has been translated by machine translation. View original

In Apple's September event, new iPhones were announced, and preparation for iOS 26.0 compatibility has begun in earnest. I've also been working on making my personally developed apps compatible with iOS 26.0.

I had been using SwiftUI's actionSheet modifier continuously until now. It's a standard UI component for action selection screens, and because it was easy to implement, I had incorporated it into my BaseViewModel. Although it became deprecated in iOS 15.0, I continued using it.

However, when running on iOS 26.0, display issues occurred. Specifically, the dialog appeared crushed.

Fortunately, I discovered this issue during preparation for the update release, but an urgent solution was needed. Since this "crushing" phenomenon is difficult to explain in words, please check the actual behavior in the GIF videos. On the left is the problematic actionSheet, and on the right is the solution using confirmationDialog.

actionSheet (with issue) confirmationDialog (after fix)
before after

In this article, I'll introduce the display issue with actionSheet in iOS 26.0 and the migration to confirmationDialog as a solution. I hope this will be helpful for those facing the same problem.

Test Environment

I conducted this verification in the following environment:

  • macOS Sequoia 15.6.1
  • Xcode 26.0
  • iOS 26.0 Simulator
  • iOS 18.5 Simulator (for comparison)

Problem Details

To clarify the issue, I created a minimal project (verification code) implementing both actionSheet and confirmationDialog to compare their behavior.

Full sample code for verification
//
//  ContentView.swift
//  SampleConfirmationDialog
//
//  Created by KENJIWADA on 2025/09/11.
//

import SwiftUI

struct ContentView: View {
    @State private var showActionSheet = false
    @State private var showConfirmationDialog = false

    var body: some View {
        NavigationView {
            VStack(spacing: 30) {
                // Button to display confirmationDialog
                Button(action: {
                    showConfirmationDialog = true
                }) {
                    Label("confirmationDialogを表示", systemImage: "text.bubble")
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.green)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
                .confirmationDialog("オプション選択", isPresented: $showConfirmationDialog) {
                    Button("オプション1") {
                    }
                    Button("オプション2") {
                    }
                    Button("削除", role: .destructive) {
                    }
                    // Cancel button is added automatically
                } message: {
                    Text("以下から選択してください")
                }

                // Button to display actionSheet
                Button(action: {
                    showActionSheet = true
                }) {
                    Label("actionSheetを表示", systemImage: "square.stack.3d.up")
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }

                Spacer()
            }
            .padding()
            .navigationTitle("Dialog比較デモ")
            .actionSheet(isPresented: $showActionSheet) {
                ActionSheet(
                    title: Text("オプション選択"),
                    message: Text("以下から選択してください"),
                    buttons: [
                        .default(Text("オプション1")) {
                        },
                        .default(Text("オプション2")) {
                        },
                        .destructive(Text("削除")) {
                        },
                        .cancel(Text("キャンセル"))
                    ]
                )
            }
        }
    }
}

#Preview {
    ContentView()
}

The sample code is also available in the following Gist:

https://gist.github.com/CH3COOH/fc6e4bc0de3004855d6253da5eecb370

Comparing behavior by running the sample code

The results of running the sample code on both iOS versions are as follows:

Version confirmationDialog actionSheet
iOS 18.5 ios18_confirmDialog_320px ios18_actionSheet_320px
iOS 26 ios26_confirmDialog_320px ios26_actionSheet_320px

Observing the behavior in iOS 26.0 in detail revealed:

  • With actionSheet, the dialog is unnaturally compressed
  • With confirmationDialog, these issues are resolved

As mentioned earlier, actionSheet and confirmationDialog had functioned equivalently, which is why I continued using the deprecated API. While they still look similar in iOS 26.0, their behaviors now differ. I considered this a strong indication to stop using actionSheet and decided to migrate to confirmationDialog.

However, I discovered that simply replacing it wasn't enough. As seen in the screenshots, implementing it following the traditional actionSheet approach leads to issues with anchor point settings. For solutions to this problem, refer to the "Popover Display Issues" in the "Troubleshooting" section below.

About confirmationDialog

Let's examine confirmationDialog, which is the migration target.

Why migrate to confirmationDialog?

Why should we stop using actionSheet and switch to confirmationDialog? Investigating Apple's reasons for promoting this change revealed these benefits:

  • Cross-platform support for macOS, watchOS, and tvOS
  • Modern API design that better aligns with SwiftUI design principles
  • Continuous support ensuring compatibility with new features
  • Flexible display control for fine-tuning elements like title visibility

I'm currently experiencing the third benefit firsthand, struggling because I hadn't switched to confirmationDialog.

Basic Syntax

First, let's look at the most basic usage of confirmationDialog.

.confirmationDialog(
    _ titleKey: LocalizedStringKey,
    isPresented: Binding<Bool>,
    titleVisibility: Visibility = .automatic,
    @ViewBuilder actions: () -> View
) -> some View

To add a message, use the following syntax:

.confirmationDialog(
    _ titleKey: LocalizedStringKey,
    isPresented: Binding<Bool>,
    titleVisibility: Visibility = .automatic,
    @ViewBuilder actions: () -> View,
    @ViewBuilder message: () -> View
) -> some View

Parameters

The role of each parameter is as follows:

  • titleKey: The dialog's title
  • isPresented: Binding that controls display state
  • titleVisibility: Controls title display
    • .automatic: System decides automatically (default)
    • .visible: Always displayed
    • .hidden: Hidden (remains for accessibility)
  • actions: Closure defining buttons
  • message: Additional message (optional)

Specific Migration Steps

As a successor API, the migration process was straightforward. Here's a typical migration pattern with concrete examples:

Before (actionSheet)

Previously, actions were defined with ActionSheet.Button.

.actionSheet(isPresented: $showSheet) {
    ActionSheet(
        title: Text("画像の選択"),
        message: Text("画像の取得方法を選択してください"),
        buttons: [
            .default(Text("カメラで撮影")) {
                openCamera()
            },
            .default(Text("ライブラリから選択")) {
                openLibrary()
            },
            .destructive(Text("画像を削除")) {
                deleteImage()
            },
            .cancel()
        ]
    )
}

After (confirmationDialog)

.confirmationDialog(
    "画像の選択",
    isPresented: $showSheet,
    titleVisibility: .visible
) {
    Button("カメラで撮影") {
        openCamera()
    }
    Button("ライブラリから選択") {
        openLibrary()
    }
    Button("画像を削除", role: .destructive) {
        deleteImage()
    }
} message: {
    Text("画像の取得方法を選択してください")
}

In confirmationDialog, you don't need to use ActionSheet.Button; instead, you can use the generic Button. For buttons requiring caution like deletion actions, specify role: .destructive. Also, you don't need to explicitly define a cancel button as it's added automatically.

Note that because of the new Liquid Glass effect introduced in iOS 26, which animates from the trigger view, attaching to an inappropriate location can cause unexpected behavior. Attach directly to the trigger view.

Troubleshooting

Popover Display Issues

I encountered an issue where anchor points weren't properly set when displayed as popovers. This problem was resolved by attaching the modifier directly to the button itself.

// WRONG: Attached to VStack
VStack {
    Button("Show Dialog") {
        showDialog = true
    }
}
.confirmationDialog(...) // Improper positioning

// CORRECT: Attached directly to Button
Button("Show Dialog") {
    showDialog = true
}
.confirmationDialog(...) // Correct positioning

By attaching directly to the Button, the anchor point emerges from the button.

anchor_320px

Handling Multiple Dialogs

When using multiple confirmationDialogs, each should be controlled with an independent @State variable. Sharing variables can lead to unexpected behavior. Be careful when you want to display multiple dialogs in a single View.

@State private var showDeleteDialog = false
@State private var showShareDialog = false // Manage separately

Conclusion

I discovered the actionSheet display issue in iOS 26.0 and implemented a migration to confirmationDialog. The migration process was easier than expected. Moreover, it resulted in providing a better user experience than before.

If you're facing the same issue, I recommend considering migration to confirmationDialog. Following the steps outlined in this article should allow for a smooth transition.

This experience made me keenly aware of the risks of ignoring deprecated APIs. Going forward, I aim to actively adapt to new iOS features and changes to provide users with the best experience possible.

I hope this article serves as a helpful reference for developers facing similar issues.

References

Share this article

FacebookHatena blogX

Related articles