davextreme
davextreme

Reputation: 1155

Hosting Controller When Using iOS 14 @main

I’m experimenting with a “pure” SwiftUI app. It doesn’t have a SceneDelegate so I’m unsure of where to put Hosting Controller stuff that I need for when it’ll be running on iOS.

Previously in the SceneDelegate I’d have code that would say something like:

let contentView = ContentView()
window.rootViewController = UIHostingController(rootView: contentView)

Now I just have an @main file with:

var body: some Scene {
    WindowGroup {
        ContentView()
    }
}

So where does the Hosting Controller stuff go (or how else can I access UIKit features that SwiftUI doesn’t have? (Specifically, I want to mess with the status bar, auto hiding the home indicator, and a few things about light/dark mode that SwiftUI’s preferredColorScheme doesn’t cover.)

Upvotes: 16

Views: 4382

Answers (6)

Slyv
Slyv

Reputation: 529

There is an issue in @Asperi code. It doesn't wait until window is really attached, and sometimes self.callback(view?.window) returns nil.

Here is a fix I made:

#if os(iOS)
private extension View {
    func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
        background(HostingWindowFinder(callback: callback))
    }
}

private struct HostingWindowFinder: UIViewRepresentable {
    var callback: (UIWindow?) -> Void

    func makeUIView(context _: Context) -> UIView {
        let view = HostedView()
        view.windowFinder = self
        return view
    }

    func updateUIView(_: UIView, context _: Context) {}

    private class HostedView: UIView {
        internal var windowFinder: HostingWindowFinder?
        override func didMoveToWindow() {
            super.didMoveToWindow()
            DispatchQueue.main.async { [weak self] in
                self?.windowFinder?.callback(self?.window)
            }
        }
    }
}

#elseif os(macOS)

private extension View {
    func withHostingWindow(_ callback: @escaping (NSWindow?) -> Void) -> some View {
        background(HostingWindowFinder(callback: callback))
    }
}

private struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow?) -> Void

    func makeNSView(context _: Context) -> NSView {
        let view = HostedView()
        view.windowFinder = self
        return view
    }

    func updateNSView(_: NSView, context _: Context) {}

    private class HostedView: NSView {
        internal var windowFinder: HostingWindowFinder?
        override func viewDidMoveToWindow() {
            super.viewDidMoveToWindow()
            DispatchQueue.main.async { [weak self] in
                self?.windowFinder?.callback(self?.window)
            }
        }
    }
}
#endif

Upvotes: 0

FPP
FPP

Reputation: 348

@Asperi

Here is the same for macOS:

extension View {
    func withHostingWindow(_ callback: @escaping (NSWindow?) -> Void) -> some View {
        self.background(HostingWindowFinder(callback: callback))
    }
}

struct HostingWindowFinder: NSViewRepresentable {
    typealias NSViewType = NSView
    
    var callback: (NSWindow?) -> ()
    
    func makeNSView(context: Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }
    
    func updateNSView(_ nsView: NSView, context: Context) {
    }
}

with the usage:

                .withHostingWindow({ window in
                    if let controller = window?.windowController {
                        controller...
                    }
                })

Upvotes: 3

Asperi
Asperi

Reputation: 258277

Here is a possible approach (tested with Xcode 12 / iOS 14)... but if you intend to use UIKit features heavily it is better to use UIKit Life-Cycle, as it gives more flexibility to configure UIKit part.

struct ContentView: View {

    var body: some View {
      Text("Demo Root Controller access")
        .withHostingWindow { window in
            if let controller = window?.rootViewController {
                // .. do something with root view controller
            }
        }
    }
}

extension View {
    func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
        self.background(HostingWindowFinder(callback: callback))
    }
}

struct HostingWindowFinder: UIViewRepresentable {
    var callback: (UIWindow?) -> ()

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

Upvotes: 18

Chris McElroy
Chris McElroy

Reputation: 511

As a potentially simpler approach, this solved the problem for me in iOS 15:

var body: some Scene {
    WindowGroup {
        ContentView()
            .onAppear {
                if let window = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first {
                    // you can now use window or window.rootViewController as needed
                }
            }
    }
}

Upvotes: 0

happycodelucky
happycodelucky

Reputation: 1011

I was facing the same problem. I played around with an alternative solution with zero set up, meaning it would work with SwiftUI App and Playgrounds (I even wrote a set of Playgrounds for documentation) - The package is called SwiftUIWindowBinder.

Example using WindowBinder... See docs for other usage, such as event view modifiers (like onTapGesture), or the convenience of WindowButton.

import SwiftUI
import SwiftUIWindowBinder

struct ContentView : View {
    /// Host window state (will be bound)
    @State var window: Window?

    var body: some View {
        // Create a WindowBinder and bind it to the state property `window`
        WindowBinder(window: $window) {
          
            Text("Hello")
                .padding()
                .onTapGesture {
                    guard let window = window else {
                        return
                    }

                    print(window.description)
                }
          
        }
    }
}

Only caveat of the package is you cannot use a host window to construct your view. I have a whole Playground page on this.

Upvotes: 6

Cory Loken
Cory Loken

Reputation: 1395

It will depend on what you are looking to change, but you can do the following modifiers on ContentView .statusBar(hidden: true). This could also be placed in parts of the app where it might make sense to hide in a specific circumstance.

This article has an great list of all the new modifiers available. https://medium.com/better-programming/swiftui-views-and-controls-the-swift-2-documentation-youve-been-waiting-for-dfa32cba24f3#6299

Upvotes: 0

Related Questions