🐈

SwiftUIでフローティングシートを作成

に公開
2026/02/16

フローティングシート

ここでいうフローティングシートはApp Storeのプレビューのようなものです。
特に名前はないのですが、名前がないと不便なのでここではフローティングシートと呼びます。

ビスターさんに教えていただきました。ありがとうございます🙏

通常のシートのサイズより小さく、浮いた形で表示しつつ裏のコンテンツは常に見れるので一時的なプレビューであることがわかりやすいです。シートのAPIを使っているので標準で下にスワイプするだけで閉じることができます。

iOS 26.1 ~

UISheetPresentationController.Detent.backgroundEffect iOS 26.1 ~

SwiftUIIntrospectを使っています。

import SwiftUI
import SwiftUIIntrospect

struct ContentView: View {
  @State var isPresented = false
  var body: some View {
    List {
      ForEach(0..<20) { i in
        Button("Button\(i)") {
          isPresented.toggle()
        }
      }
    }
    .sheet(isPresented: $isPresented) {
      Image(.character)
        .resizable()
        .scaledToFit()
        .overlay(alignment: .topTrailing) {
          Button(role: .close) {
            isPresented.toggle()
          }
          .labelStyle(.iconOnly)
          .buttonStyle(.glass)
          .buttonBorderShape(.circle)
          .controlSize(.large)
          .padding()
        }
        .clipShape(.rect(cornerRadius: 24))
        .padding(30)
        .introspect(.sheet, on: .iOS(.v26)) { sheet in
          sheet.backgroundEffect = UIBlurEffect(style: .systemMaterial)
        }
    }
  }
}

iOS 26.0

sheetではなく、fullScreenCoverとpresentationBackground(.clear)を使えば見た目は実装できるのですが、スワイプで閉じることができないです。(この手法はiOS 26.1以降でも使用可能です)

struct ContentView: View {
  @State var isPresented = false
  var body: some View {
    List {
      ForEach(0..<20) { i in
        Button("Button\(i)") {
          isPresented.toggle()
        }
      }
    }
    .fullScreenCover(isPresented: $isPresented) {
      Image(.character)
        .resizable()
        .scaledToFit()
        .overlay(alignment: .topTrailing) {
          Button(role: .close) {
            isPresented.toggle()
          }
          .labelStyle(.iconOnly)
          .buttonStyle(.glass)
          .buttonBorderShape(.circle)
          .controlSize(.large)
          .padding()
        }
        .clipShape(.rect(cornerRadius: 24))
        .padding(30)
        .presentationBackground(.clear)
    }
  }
}

iOS 16.4 ~ 18.x

iOS 18以下はpresentationBackground(.clear)がsheetにも効くので一番実装は簡単です。

struct ContentView: View {
  @State var isPresented = false
  var body: some View {
    List {
      ForEach(0..<20) { i in
        Button("Button\(i)") {
          isPresented.toggle()
        }
      }
    }
    .sheet(isPresented: $isPresented) {
      Image(.character)
        .resizable()
        .scaledToFit()
        .overlay(alignment: .topTrailing) {
          Button {
            isPresented.toggle()
          } label: {
            Image(systemName: "xmark")
          }
          .buttonStyle(.bordered)
          .buttonBorderShape(.circle)
          .controlSize(.large)
          .padding()
        }
        .clipShape(.rect(cornerRadius: 24))
        .padding(30)
        .presentationBackground(.clear)
    }
  }
}

ナビゲーションアニメーション

App Storeのシートの表示にはズームアニメーションがかかっているので、matchedTransitionSource, navigationTransitionを使って再現しようとしたのですが、背景色が暴れてうまくいきませんでした...

struct ContentView: View {
  @Namespace var namespace
  @State var isPresented = false
  var body: some View {
    List {
      Image(.banner)
        .resizable()
        .scaledToFit()
        .onTapGesture {
          isPresented.toggle()
        }
        .matchedTransitionSource(id: "image", in: namespace)
    }
    .sheet(isPresented: $isPresented) {
      Image(.character)
        .resizable()
        .scaledToFit()
        .overlay(alignment: .topTrailing) {
          Button(role: .close) {
            isPresented.toggle()
          }
          .labelStyle(.iconOnly)
          .buttonStyle(.glass)
          .buttonBorderShape(.circle)
          .controlSize(.large)
          .padding()
        }
        .clipShape(.rect(cornerRadius: 24))
        .padding(30)
        .introspect(.sheet, on: .iOS(.v26)) { sheet in
          sheet.backgroundEffect = UIBlurEffect(style: .systemMaterial)
        }
        .navigationTransition(.zoom(sourceID: "image", in: namespace))
    }
  }
}

Discussion