Hi DmitryKurkin,
This appears to be a known issue (r. 115856582) on iOS 17 affecting sheet
and fullScreenCover
presentation. As a workaround, you can use bridge to UIKit to create your own presentation controllers above your SwiftUI content (preventing the memory retention issue). Please see the following code snippet as a guide:
import SwiftUI
enum SheetPresenterStyle {
case sheet
case popover
case fullScreenCover
case detents([UISheetPresentationController.Detent])
}
class SheetWrapperController: UIViewController {
let style: SheetPresenterStyle
init(style: SheetPresenterStyle) {
self.style = style
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
if case let (.detents(detents)) = style, let sheetController = self.presentationController as? UISheetPresentationController {
sheetController.detents = detents
sheetController.prefersGrabberVisible = true
}
}
}
struct SheetPresenter<Content>: UIViewRepresentable where Content: View {
let label: String
let content: () -> Content
let style: SheetPresenterStyle
init(_ label: String, style: SheetPresenterStyle, @ViewBuilder content: @escaping () -> Content) {
self.label = label
self.content = content
self.style = style
}
func makeUIView(context: UIViewRepresentableContext<SheetPresenter>) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(label, for: .normal)
let action = UIAction { _ in
let hostingController = UIHostingController(rootView: content())
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let viewController = SheetWrapperController(style: style)
switch style {
case .sheet:
viewController.modalPresentationStyle = .automatic
case .popover:
viewController.modalPresentationStyle = .popover
viewController.popoverPresentationController?.sourceView = button
case .fullScreenCover:
viewController.modalPresentationStyle = .fullScreen
case .detents:
viewController.modalPresentationStyle = .automatic
}
viewController.addChild(hostingController)
viewController.view.addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: viewController.view.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor),
])
hostingController.didMove(toParent: viewController)
if let rootVC = button.window?.rootViewController {
rootVC.present(viewController, animated: true)
}
}
button.addAction(action, for: .touchUpInside)
return button
}
func updateUIView(_ uiView: UIButton, context: Context) {}
}
typealias ContentView = ContentViewB
struct ContentViewA: View {
@State private var showSheet = false
@State private var showPopover = false
@State private var showFullScreenCover = false
var body: some View {
VStack {
Button("Present Sheet") { showSheet.toggle() }
Button("Present Popover") { showPopover.toggle() }
Button("Present Full Screen Cover") { showFullScreenCover.toggle() }
Text("First")
.sheet(isPresented: $showSheet) {
SheetView()
}
.popover(isPresented: $showPopover) {
PopoverView()
}
.fullScreenCover(isPresented: $showFullScreenCover) {
FullScreenCoverView()
}
}
}
}
struct ContentViewB: View {
var body: some View {
VStack {
SheetPresenter("Present Sheet", style: .sheet) {
SheetView()
}
SheetPresenter("Present Popover", style: .popover) {
PopoverView()
}
SheetPresenter("Present Full Screen Cover", style: .fullScreenCover) {
FullScreenCoverView()
}
SheetPresenter("Present Presentation Detents", style: .detents([.medium(), .large()])) {
PresentationDetentsView()
}
Text("First")
}
}
}
struct SheetView: View {
private let log = LifecycleLogger(name: "SheetView")
@Environment(\.dismiss) var dismiss
var body: some View {
Text("SheetView")
Button("Back") { dismiss() }
}
}
struct PopoverView: View {
private let log = LifecycleLogger(name: "PopoverView")
@Environment(\.dismiss) var dismiss
var body: some View {
Text("PopoverView")
Button("Back") { dismiss() }
}
}
struct FullScreenCoverView: View {
private let log = LifecycleLogger(name: "FullScreenCoverView")
@Environment(\.dismiss) var dismiss
var body: some View {
Text("FullScreenCoverView")
Button("Back") { dismiss() }
}
}
struct PresentationDetentsView: View {
private let log = LifecycleLogger(name: "PresentationDetentsView")
@Environment(\.dismiss) var dismiss
var body: some View {
Text("PresentationDetentsView")
Button("Back") { dismiss() }
}
}
class LifecycleLogger {
let name: String
init(name: String) {
self.name = name
print("\(name).init")
}
deinit {
print("\(name).deinit")
}
}
Cheers,
Paris