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)
}
}
}
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
}
}
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")
}
}
}
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}
)
}
}
}
}
Any suggestions or references for how to implement this properly would be greatly appreciated!
It's possible technically, but the way I've managed to make it work is fragile and ugly. The idea is to
That is:
UIApplication.shared.windows.first?.addSubview(settingsButton)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
UIApplication.shared.windows.first?.bringSubviewToFront(settingsButton)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With