Reputation: 1155
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
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
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
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
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
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
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