決済認証システム開発事業部の冨永です。
現在、主にiOS/iPadアプリの開発を担当しております。
今回は、Xcode16.2・iOS16.3~iOS18.4までの.sheet(isPresented:)にて空のモーダルが表示される挙動とその解決方法について書きたいと思います。
目次
はじめに
Xcode16.2でiOS16.4〜iOS18.4をターゲットにした際に、 SwiftUIの.sheet(isPresented:)を利用してシートを表示させる際、初回の表示が正常に行われない挙動が確認されました。
以下のサンプルコードを使用して、問題の再現手順と解決策を解説します。
import SwiftUI struct ContentView: View { @State private var isSheetPresented: Bool = false @State private var item: String? var body: some View { VStack { Button("present A") { item = "A" isSheetPresented = true } Button("present B") { item = "B" isSheetPresented = true } } .sheet(isPresented: $isSheetPresented) { if let item { SheetView(item: item) .presentationBackgroundInteraction(.enabled) .presentationDetents([.height(300)]) } } } } struct SheetView: View { let item: String var body: some View { Text(item) .frame(maxWidth: .infinity,maxHeight: .infinity) .background(.green) } } #Preview { ContentView() }
挙動の詳細
上記のコードをXcode16.2・iOS18.1~iOS18.4で動作させた場合、初回に"present A"ボタンを押してもハーフモーダルが表示されず、空の画面になります。
その後、"present B"ボタンを押すと、その後"present A"を押した際も正常にシートが表示される現象が確認されます。この挙動はAppleに対してフィードバック済みです。

解決策
解決策は2種類ございます。
1. sheet(item:)を利用する方法
sheet(item:)の使用方法については、Identifiableに準拠させる必要があります。 以下のように、itemをIdentifiableに変更し、.sheet(item:)を使用することで問題を解決できます。
import SwiftUI struct ContentView: View { @State private var item: SheetItem? var body: some View { VStack { Button("present A") { item = SheetItem(content: "A") } Button("present B") { item = SheetItem(content: "B") } } .sheet(item: $item) { selectedItem in SheetView(item: selectedItem.content) .presentationBackgroundInteraction(.enabled) .presentationDetents([.height(300)]) } } } struct SheetItem: Identifiable { let id: UUID = UUID() let content: String } struct SheetView: View { let item: String var body: some View { Text(item) } } #Preview { ContentView() }

ここで注意点があります。
idにUUIDを指定した場合、SheetItemが変更されるたびにシートが一度閉じる挙動になります。
iOS18.1~iOS18.4の挙動として、この際にハーフモーダルの高さがLargeに切り替わってしまう現象が確認されています。

この問題を回避するために、UUIDではなく固定のIDをIdentifiableに付与します。
struct SheetItem: Identifiable { let id : Int = 1 // 固定のID // let id: UUID = UUID() let content: String }

2. SheetViewの中身をBindingにする方法
SheetViewを以下のようにBindingを使って再設計することで解決が可能です。 ただし、ifの分岐にEmptyViewがある分、不安は残ります。
import SwiftUI struct ContentView: View { @State private var isSheetPresented: Bool = false @State private var item: String? var body: some View { VStack { Button("present A") { item = "A" isSheetPresented = true } Button("present B") { item = "B" isSheetPresented = true } } .sheet(isPresented: $isSheetPresented) { SheetView(item: $item) .presentationBackgroundInteraction(.enabled) .presentationDetents([.height(300)]) } } } struct SheetView: View { @Binding var item: String? // Bindingにする var body: some View { if let item { Text(item) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.green) } else { EmptyView() } } } #Preview { ContentView() }

まとめ
今回の挙動はXcode16.2およびiOS16.4~18.4の特定バージョンにおいて確認されており、Appleにもフィードバックを送信済みです。
解決策としては、1. sheet(item:)を使う方法を推奨しますが、実装内容によっては2. Bindingを使った実装も有効です。
ぜひ、ご参考になればと思います。
テコテックの採用活動について
テコテックでは新卒採用、中途採用共に積極的に募集をしています。 採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。 ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp