Хостинг-контроллер при использовании 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
                }
            }
    }
}
Другие вопросы по тегам