Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overlay SwiftUI view over all other views including sheets

Tags:

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)
        }
    }
}

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!

like image 582
Sparklebeard Avatar asked Dec 11 '19 00:12

Sparklebeard


1 Answers

It's possible technically, but the way I've managed to make it work is fragile and ugly. The idea is to

  1. add the button as a child of app's key window and
  2. bring it front every time a sheet is presented. Specifically, it must be brought to front with some delay after sheet presentation is started.

That is:

  1. UIApplication.shared.windows.first?.addSubview(settingsButton)
  2. When any sheet is about to be presented, notify the owner of settingsButton so that it brings the button to front:

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

like image 173
Russian Avatar answered Oct 17 '22 16:10

Russian