JohnSF
JohnSF

Reputation: 4330

SwiftUI Load View from SceneDelegate sceneDidBecomeActive

I'm trying to understand how to load a SwiftUI view from Swift function code. In this case specifically, I want to load a view when returning from the background state to cover sensitive data. I have created a biometric login and that works fine - pure SwiftUI views for the app. When I put the app into the background and return, the FaceID works as expected, but the underlying screen is visible. This is a generalized question too - how can you load any SwiftUI view from any Swift function.

func sceneDidBecomeActive(_ scene: UIScene) {

    if userDefaultsManager.wentToBackground {
        if userDefaultsManager.enableBiometrics {
            BiometricsLogin(userDefaultsManager: userDefaultsManager).authenticate()
            //what I want is something like:
            //BiometricsLogin(userDefaultsManager: userDefaultsManager)
            //kinda like you would do in a TabView
            //that would run the authentication just like starting the app
            userDefaultsManager.wentToBackground = false
        }
    }
}

And the login code is pretty generic:

struct BiometricsLogin: View {
    @ObservedObject var userDefaultsManager: UserDefaultsManager

    var body: some View {
        NavigationView {
            VStack {
                Image("CoifMeCrop180")
                .onAppear {
                    self.authenticate()
                }
            }//vstack
        }//nav
    }

    func authenticate() {
        let context = LAContext()
        var error: NSError?

        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "The app uses Biometrics to unlock you data"
            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { (success, authenticationError)
                in
                DispatchQueue.main.async {
                    if success {
                        self.userDefaultsManager.isAuthenticated = true
                        self.userDefaultsManager.selectedTab = 1
                    } else {
                        if self.userDefaultsManager.enableBiometrics {
                            self.userDefaultsManager.isAuthenticated = false
                            self.userDefaultsManager.selectedTab = 3
                        } else {
                            self.userDefaultsManager.isAuthenticated = false
                            self.userDefaultsManager.selectedTab = 1
                        }
                    }
                }
            }
        } else {
            //no biometrics - deal with this elsewhere
            //consider an alert here
        }
    }//authenticate
}

I also tried using a hosting controller like this, but it did not work either. Same issue, the authentication worked but the data was visible.

    //this does not work
    let controller = UIHostingController(rootView: BiometricsLogin(userDefaultsManager: userDefaultsManager))
    self.window!.addSubview(controller.view)
    self.window?.makeKeyAndVisible()

Any guidance would be appreciated. Xcode 11.3.1 (11C504)

Upvotes: 3

Views: 1873

Answers (1)

Asperi
Asperi

Reputation: 258365

Here is possible approach

In sceneDidBecomeActive add presenting new controller with authentication in full screen to hide sensitive content

    func sceneDidBecomeActive(_ scene: UIScene) {
        let controller = UIHostingController(rootView: BiometricsLogin(userDefaultsManager: userDefaultsManager))
        controller.modalPresentationStyle = .fullScreen

        self.window?.rootViewController?.present(controller, animated: false)
    }

now it needs to dismiss it when authentication will be done, so add notification for that purpose...

extension SceneDelegate {
    static let didAuthenticate = Notification.Name(rawValue: "didAuthenticate")
}

... and subscriber for it in SceneDelegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    private var authenticateObserver: AnyCancellable?

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

        let contentView = ContentView()

        if let windowScene = scene as? UIWindowScene {
            self.authenticateObserver = NotificationCenter.default.publisher(for: SceneDelegate.didAuthenticate)
                .sink { _ in
                    self.window?.rootViewController?.dismiss(animated: true)
                }

            let window = UIWindow(windowScene: windowScene)
            ...

when authentication is done just post didAuthenticate notification to dismiss top controller

   DispatchQueue.main.async {
        if success {
            self.userDefaultsManager.isAuthenticated = true
            self.userDefaultsManager.selectedTab = 1
        } else {
            if self.userDefaultsManager.enableBiometrics {
                self.userDefaultsManager.isAuthenticated = false
                self.userDefaultsManager.selectedTab = 3
            } else {
                self.userDefaultsManager.isAuthenticated = false
                self.userDefaultsManager.selectedTab = 1
            }
        }
        NotificationCenter.default.post(name: SceneDelegate.didAuthenticate, object: nil)
    }

Upvotes: 1

Related Questions