Overlay SwiftUI view over all other views including sheets

Spa*_*ard 10 ios swift swiftui

I have seen discussion about this that involve UIKit but nothing recent that includes SwiftUI.

I have a Master-Detail style app that calls for two floating buttons that should be visible at all times in the app.

Settings Button: When tapped will un-hide another overlay with some toggles Add Record Button: When tapped will present a sheet via a @State variable, when tapped again will dismiss the sheet.

If I set the buttons as overlays on the NavigationView they get pushed into the background when the sheet it presented. This isn't particularly surprising but it is not the behaviour that is called for in the design.

First Approach - .overlay on NavigationView()

struct ContentView: View {
    @State var addRecordPresented: Bool = false
      var body: some View {

        NavigationView {
            VStack {
                SomeView()
                AnotherView()
                    .sheet(isPresented: $addRecordPresented, onDismiss: {self.addRecordPresented = false}) {AddRecordView()}
            }
            .overlay(NewRecordButton(isOn: $addRecordPresented).onTapGesture{self.addRecordPresented.toggle()}, alignment: .bottomTrailing)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Second Approach - overlay as second UIWindow

I then started again and attempted to create a second UIWindow in SceneDelegate which contained a ViewController hosting the SwiftUI view within UIHostingController, however I had no success trying to override in order to allow both the button to be tappable, but for other taps to be passed through to the window behind the overlay window.

Data flow stuff is removed, this is just trying to present a floating tappable circle that will toggle between green and red when tapped, and has a purple square in the main content view that will present a yellow sheet when tapped. The circle correctly floats on top of the sheet however never responds to taps.

In SceneDelegate:

    var window: UIWindow?
    var wimdow2: UIWindow2?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

           (...)


        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window

            window.windowLevel = .normal
            window.makeKeyAndVisible()

            let window2 = UIWindow2(windowScene: windowScene)
            window2.rootViewController = OverlayViewController()

            self.window2 = window2
            window2.windowLevel = .normal+1
            window2.isHidden = false
        }
    }


            (...)


        class UIWindow2: UIWindow {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        let hitView = super.hitTest(point, with: event)

        if hitView != self {
            return nil
        }
        return hitView
    }
}
Run Code Online (Sandbox Code Playgroud)

in a ViewController file:

import UIKit
import SwiftUI

class OverlayViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()


        let overlayView = UIHostingController(rootView: NewRecordButton())
        addChild(overlayView)

        overlayView.view.backgroundColor = UIColor.clear
        overlayView.view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        overlayView.view.isUserInteractionEnabled = true

        self.view.addSubview(overlayView.view)
        overlayView.didMove(toParent: self)

    }

}

struct NewRecordButton: View {
    @State var color = false
    var body: some View {
        Circle().foregroundColor(color ? .green : .red).frame(width: 50, height: 50).onTapGesture {
            self.color.toggle()
            print("tapped circle")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Plain vanilla swiftUI view in the main content window:

import SwiftUI

struct ContentView: View {
    @State var show: Bool = false
    var body: some View {

        NavigationView {
            VStack {
                Rectangle().frame(width: 100, height: 100).foregroundColor(.purple).onTapGesture {self.show.toggle()}
                Text("Tap for Yellow").sheet(isPresented: $show, content: {Color.yellow}
                )
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Any suggestions or references for how to implement this properly would be greatly appreciated!

Rus*_*ian 2

从技术上讲这是可能的,但我设法让它发挥作用的方式是脆弱且丑陋的。这个想法是

  1. 将按钮添加为应用程序关键窗口的子级,然后
  2. 每次提交表格时都将其放在前面。具体来说,在工作表呈现开始后,必须将其置于前面并延迟一些。

那是:

  1. UIApplication.shared.windows.first?.addSubview(settingsButton)
  2. 当任何工作表即将显示时,通知 settingsButton 的所有者,以便它将按钮置于前面:

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { UIApplication.shared.windows.first?.bringSubviewToFront(settingsButton) }

  • 这可能为时已晚,但仅供参考,正确的方法是使用“definesPresentationContext”之类的东西。使用它,您可以创建自己的“容器”视图控制器,其中包含主 VC 和按钮。然后,当演示发生时,它将发生在 _your_ 容器内,并将按钮保持在顶部。您可以通过使用简单的选项卡栏控制器设置来测试这一点,在选项卡的控制器之一上设置“definesPresentationContext”,然后从中进行呈现。您会看到选项卡选项卡保留在顶部(与默认行为不同)。 (2认同)
  • 仅供参考,您选择的方法不一定能够正确响应 VC 生命周期事件,通常包括旋转和安全区域插入。我刚刚建议的方法解决了所有这些问题:) (2认同)