Хостинг-контроллер при использовании iOS 14 @main
Я экспериментирую с "чистым" приложением SwiftUI. У него нетSceneDelegate
поэтому я не уверен, где разместить вещи Hosting Controller, которые мне нужны, когда они будут работать на iOS.
Ранее в SceneDelegate
У меня был код, который говорил бы что-то вроде:
let contentView = ContentView()
window.rootViewController = UIHostingController(rootView: contentView)
Теперь у меня просто есть @main
файл с:
var body: some Scene {
WindowGroup {
ContentView()
}
}
Итак, где находится материал Hosting Controller (или как еще я могу получить доступ к функциям UIKit, которых нет в SwiftUI? (В частности, я хочу возиться со строкой состояния, автоматически скрывать домашний индикатор и кое-что о светлом / темном) режим, который SwiftUI's preferredColorScheme
не покрывает.)
6 ответов
Вот возможный подход (протестирован с Xcode 12 / iOS 14)... но если вы намерены интенсивно использовать функции UIKit, лучше использовать жизненный цикл UIKit, поскольку он дает больше гибкости для настройки части UIKit.
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) {
}
}
Я столкнулся с той же проблемой. Я поигрался с альтернативным решением с нулевой настройкой, то есть оно будет работать с приложением SwiftUI и игровыми площадками (я даже написал набор игровых площадок для документации) - пакет называется SwiftUIWindowBinder.
Пример использования
WindowBinder
... См. Документацию о другом использовании, например о модификаторах представления событий (например,
onTapGesture
) или удобство
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)
}
}
}
}
Единственное предостережение пакета в том, что вы не можете использовать хост-окно для создания своего представления. У меня есть целая страничка по этому поводу.
@ Аспери: очень мило, спасибо.
Вот то же самое для 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) {
}
}
с использованием:
.withHostingWindow({ window in
if let controller = window?.windowController {
controller...
}
})
Это будет зависеть от того, что вы хотите изменить, но вы можете использовать следующие модификаторы для ContentView
.statusBar(hidden: true)
. Это также может быть размещено в частях приложения, где имеет смысл скрыть в определенных обстоятельствах.
В этой статье есть отличный список всех доступных новых модификаторов. https://medium.com/better-programming/swiftui-views-and-controls-the-swift-2-documentation-youve-been-waiting-for-dfa32cba24f3
Проблема в коде @Asperi. Он не ждет, пока окно действительно прикрепится, и иногда self.callback(view?.window) возвращает ноль.
Вот исправление, которое я сделал:
#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
В качестве потенциально более простого подхода это решило проблему для меня в 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
}
}
}
}