Zenn
🦋

SwiftUI: NSWindow. isMovableByWindowBackground有効時の当たり判定

2025/03/02に公開

NSWindowNSHostingViewでSwiftUIのViewを埋め込む場合、isMoveableByWindowBackgroundにtrueを指定するとSwiftUIのViewの当たり判定が(ButtonListなどの直接的な当たり判定を持つコンポーネントを除き)基本的に消滅します。


左:draggableなアイテムをドラッグできる、右:draggableなアイテムをドラッグできない

例えば、上のようにisMoveableByWindowBackground = trueになっていると、draggable()を適用したViewのドラッグイベントがウインドウの背景を掴んだイベントに乗っ取られて、Viewをドラッグできなくなってしまいます。

実装例

NSWindowをSwiftUIで扱いやすくするためにWindowSceneKitを使っています。

import SwiftUI
import WindowSceneKit

@main
struct TestCode_macOSApp: App {
    @WindowState("sample") var isPresented = true

    var body: some Scene {
        WindowScene(isPresented: $isPresented, window: { _ in
            CustomWindow(isMovableByWindowBackground: true, content: { ContentView() })
        })
        WindowScene(isPresented: $isPresented, window: { _ in
            CustomWindow(isMovableByWindowBackground: false, content: { ContentView() })
        })
    }
}

final class CustomWindow: NSWindow {
    init<Content: View>(isMovableByWindowBackground: Bool, @ViewBuilder content: () -> Content) {
        super.init(
            contentRect: .zero,
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false
        )
        level = .floating
        collectionBehavior = [.canJoinAllSpaces]
        self.isMovableByWindowBackground = isMovableByWindowBackground
        contentView = NSHostingView(rootView: content())
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Button {
                print("push")
            } label: {
                Text("Push")
            }
            Rectangle()
                .frame(width: 40, height: 40)
                .draggable(Item(value: 0))
        }
        .fixedSize()
        .padding()
    }
}

struct Item: Codable, Transferable {
    var value: Int

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(for: Item.self, contentType: .data)
    }
}

おそらくAppKitでのNSTrackingAreaなどの制御が原因なのではと思っていますが、SwiftUIのAPIでこの不具合をどうにかする正攻法が見つかりません。

ただ、ButtonやListなどは当たり判定があるので、邪道なテクニックを使えば無理やり当たり判定を生むことはできます。

struct ContentView: View {
    var body: some View {
        VStack {
            ZStack {
                Button(action: {}) {
                    Color.clear.frame(maxWidth: .infinity, maxHeight: .infinity)
                }
                .buttonStyle(.borderless)
                .disabled(true)
                Rectangle()
                    .frame(width: 40, height: 40)
                    .draggable(Item(value: 0))
            }
            // または
            Rectangle()
                .frame(width: 40, height: 40)
                .draggable(Item(value: 0))
                .background {
                    Button(action: {}) {
                        Color.clear.frame(maxWidth: .infinity, maxHeight: .infinity)
                    }
                    .buttonStyle(.borderless)
                    .disabled(true)
                }
        }
        .fixedSize()
        .padding()
    }
}

ZStackの下層またはbackgroundに見えない面積の広い無効なButtonを置くことでその領域にSwiftUIのViewへの当たり判定を与えることができます。

もう少し何かいい方法があればご教授いただけるとありがたいです。

Discussion

ログインするとコメントできます