🦋
SwiftUI: NSWindow. isMovableByWindowBackground有効時の当たり判定
NSWindow
にNSHostingView
でSwiftUIのViewを埋め込む場合、isMoveableByWindowBackground
にtrueを指定するとSwiftUIのViewの当たり判定が(Button
やList
などの直接的な当たり判定を持つコンポーネントを除き)基本的に消滅します。
左: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