Как получить доступ к собственному окну в представлении SwiftUI? - PullRequest
1 голос
/ 23 февраля 2020

Цель состоит в том, чтобы иметь легкий доступ к окну хостинга на любом уровне иерархии представлений SwiftUI. Цель может быть другой - закрыть окно, уйти в отставку первого респондента, заменить root view или contentViewController. Интеграция с UIKit / AppKit также иногда требует пути через окно, поэтому…

То, что я встречал здесь и пытался раньше,

что-то вроде этого

let keyWindow = shared.connectedScenes
        .filter({$0.activationState == .foregroundActive})
        .map({$0 as? UIWindowScene})
        .compactMap({$0})
        .first?.windows
        .filter({$0.isKeyWindow}).first

или через добавленный в каждый просмотр SwiftUI UIViewRepresentable / NSViewRepresentable для получения окна с использованием view.window выглядит уродливо, тяжело и непригодно для использования.

Таким образом, как мне это сделать?

Ответы [ 2 ]

3 голосов
/ 23 февраля 2020

Вот результат моих экспериментов, который выглядит подходящим для меня, так что можно было бы также найти его полезным. Протестировано с Xcode 11.2 / iOS 13.2 / macOS 15.0

Идея состоит в том, чтобы использовать концепцию среды SwiftUI, потому что однажды введенное значение среды автоматически становится доступным для всей иерархии представления. Итак

1) Определите ключ окружения. Обратите внимание, что следует помнить, чтобы избежать циклического обращения ссылок в сохраненном окне

struct HostingWindowKey: EnvironmentKey {

#if canImport(UIKit)
    typealias WrappedValue = UIWindow
#elseif canImport(AppKit)
    typealias WrappedValue = NSWindow
#else
    #error("Unsupported platform")
#endif

    typealias Value = () -> WrappedValue? // needed for weak link
    static let defaultValue: Self.Value = { nil }
}

extension EnvironmentValues {
    var hostingWindow: HostingWindowKey.Value {
        get {
            return self[HostingWindowKey.self]
        }
        set {
            self[HostingWindowKey.self] = newValue
        }
    }
}

2) Вставить окно хостинга в root ContentView вместо создания окна (либо в AppDelegate, либо в SceneDelegate, только один раз

// window created here

let contentView = ContentView()
                     .environment(\.hostingWindow, { [weak window] in
                          return window })

#if canImport(UIKit)
        window.rootViewController = UIHostingController(rootView: contentView)
#elseif canImport(AppKit)
        window.contentView = NSHostingView(rootView: contentView)
#else
    #error("Unsupported platform")
#endif

3) использовать только при необходимости, просто объявив переменную окружения

struct ContentView: View {
    @Environment(\.hostingWindow) var hostingWindow

    var body: some View {
        VStack {
            Button("Action") {
                // self.hostingWindow()?.close() // macOS
                // self.hostingWindow()?.makeFirstResponder(nil) // macOS
                // self.hostingWindow()?.resignFirstResponder() // iOS
                // self.hostingWindow()?.rootViewController?.present(UIKitController(), animating: true)
            }
        }
    }
}
0 голосов
/ 19 марта 2020

Сначала мне понравился ответ, данный @Asperi, но когда я попробовал его в своей среде, мне было трудно работать из-за необходимости знать представление root во время создания окна (следовательно, я не знаю окно в то время, когда я создаю root представление). Поэтому я последовал его примеру, но вместо значения среды я выбрал использование объекта среды. Это имеет почти такой же эффект, но мне было легче начать работать. Ниже приведен код, который я использую. Обратите внимание, что я создал обобщенный класс c, который создает NSWindowController с учетом представления SwiftUI. (Обратите внимание, что userDefaultsManager - это еще один объект, который мне нужен в большинстве windows в моем приложении. Но я думаю, что если вы удалите эту строку плюс строку appDelegate, вы получите решение, которое будет работать в значительной степени в любом месте.)

class RootViewWindowController<RootView : View>: NSWindowController {
    convenience init(_ title: String,
                     withView rootView: RootView,
                     andInitialSize initialSize: NSSize = NSSize(width: 400, height: 500))
    {
        let appDelegate: AppDelegate = NSApplication.shared.delegate as! AppDelegate
        let windowWrapper = NSWindowWrapper()
        let actualRootView = rootView
            .frame(width: initialSize.width, height: initialSize.height)
            .environmentObject(appDelegate.userDefaultsManager)
            .environmentObject(windowWrapper)
        let hostingController = NSHostingController(rootView: actualRootView)
        let window = NSWindow(contentViewController: hostingController)
        window.setContentSize(initialSize)
        window.title = title
        windowWrapper.rootWindow = window
        self.init(window: window)
    }
}

final class NSWindowWrapper: ObservableObject {
    @Published var rootWindow: NSWindow? = nil
}

Затем, на мой взгляд, где мне это нужно (чтобы закрыть окно в соответствующее время), моя структура начинается следующим образом:

struct SubscribeToProFeaturesView: View {
    @State var showingEnlargedImage = false
    @EnvironmentObject var rootWindowWrapper: NSWindowWrapper

    var body: some View {
        VStack {
            Text("Professional Version Upgrade")
                .font(.headline)
            VStack(alignment: .leading) {

И в кнопке, где мне нужно закрыть окно, у меня есть

self.rootWindowWrapper.rootWindow?.close()

Это не так чисто, как мне хотелось бы (я бы предпочел, чтобы у меня было решение, в котором я просто сказал self.rootWindow?.close() требует наличия класса-обертки), но это неплохо, и это позволяет мне создать объект rootView, прежде чем я создаю окно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...