web-dev-qa-db-ja.com

SwiftUIビュー内の独自のウィンドウにアクセスする方法は?

目標は、SwiftUIビュー階層の任意のレベルでホスティングウィンドウに簡単にアクセスできるようにすることです。目的は異なる場合があります-ウィンドウを閉じる、ファーストレスポンダを辞任する、ルートビューまたは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
Asperi

これは私にとって適切であると思われる私の実験の結果です。そのため、役立つかもしれません。 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)ホスティングウィンドウをウィンドウ作成の代わりにルート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)
            }
        }
    }
}
7
Asperi

最初は@Asperiの答えが好きでしたが、自分の環境で試してみると、ウィンドウを作成するときにルートビューを知る必要があるため、作業が難しいことがわかりました(そのため、ルートビューを作成するときのウィンドウ)。したがって、私は彼の例に従いましたが、環境値の代わりに、環境オブジェクトを使用することを選択しました。これはほとんど同じ効果がありますが、私が作業しやすくなりました。以下は私が使用するコードです。 SwiftUIビューを指定してNSWindowControllerを作成する汎用クラスを作成したことに注意してください。 (userDefaultsManagerは、アプリケーションのほとんどのウィンドウで必要な別のオブジェクトであることに注意してください。ただし、その行と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オブジェクトを作成します。

0

ウィンドウをプロパティとして環境オブジェクトに追加します。これは、他のアプリ全体のデータに使用する既存のオブジェクトにすることができます。

final class AppData: ObservableObject {
    let window: UIWindow? // Will be nil in SwiftUI previewers

    init(window: UIWindow? = nil) {
        self.window = window
    }
}

環境オブジェクトを作成するときにプロパティを設定します。ルートビューなど、ビュー階層のベースにあるビューにオブジェクトを追加します。

let window = UIWindow(windowScene: windowScene) // Or however you initially get the window
let rootView = RootView().environmentObject(AppData(window: window))

最後に、ビューでウィンドウを使用します。

struct MyView: View {
    @EnvironmentObject private var appData: AppData
    // Use appData.window in your view's body.
}
0
Edward Brey