如何重现这条 Xcode 蓝色拖线

Posted

技术标签:

【中文标题】如何重现这条 Xcode 蓝色拖线【英文标题】:How to reproduce this Xcode blue drag line 【发布时间】:2017-04-28 20:31:19 【问题描述】:

我想在我的应用中重现 Xcode 蓝色拖线。

你知道如何编码吗?

我知道如何使用 Core Graphics 画线... 但是这条线必须在所有其他项目的顶部(在屏幕上)。

【问题讨论】:

画一条线应该很简单——在所有东西上添加一个不可见的图层或视图,然后画线。 (至少这是一种方式。)但是智能地绘制线(从视图下的一个特定点到另一个这样的点),或者在这个不可见视图下的视图上进行各种操作接收这些操作将需要很多。 感谢您的回答!那么,如何在所有内容(甚至其他应用程序)之上添加一个不可见的层? 其他应用程序?我不知道。你的问题从来没有说过,Xcode 是一个单一的应用程序。我确实注意到您将其标记为 Cocoa,所以我假设您在询问 macOS,您在哪里有单独的窗口。但即使在那里,开发人员唯一一次能够在 Xcode 中“画一条蓝线”是 IIRC 版本 5,当时他们为应用程序提供了一个“统一”窗口。正如我在第一条评论中所说的那样 - easy 就是那个层。 (1) 为文件层次结构、代码编辑器、工具栏等使用带有子视图的单个或统一窗口。添加 - 最重要的是 - 另一个要绘制的子视图。 这不是我要找的。在 Xcode 中,蓝线甚至可以在应用程序之外。在我的第二个屏幕截图中,左侧是 Xcode,右侧是 Chrome,蓝线位于两个应用程序上方。我说:“在所有其他项目之上(在屏幕上)。”在屏幕上意味着所有打开的应用程序 + 桌面。我刚刚找到了一种方法,使用不可见的 NSWindow !完成后我将发布我的代码 【参考方案1】:

我在您发布自己的答案后发布此内容,因此这可能是在浪费大量时间。但是您的回答仅涵盖在屏幕上绘制一条非常简单的线,而没有涵盖您需要注意以真正复制 Xcode 的行为甚至超越它的一堆其他有趣的东西:

像 Xcode 那样画一条漂亮的连接线(带有阴影、轮廓和大圆头), 在多个屏幕上画线, 使用 Cocoa 拖放来查找拖动目标并支持弹簧加载。

这是我将在此答案中解释的演示:

In this github repo,您可以找到一个 Xcode 项目,其中包含此答案中的所有代码以及运行演示应用所需的剩余胶水代码。

像 Xcode 那样画一条漂亮的连接线

Xcode 的连接线看起来像old-timey barbell。它有一个任意长度的直条,两端各有一个圆形铃铛:

我们对这个形状了解多少?用户通过拖动鼠标提供起点和终点(铃铛的中心),我们的用户界面设计师指定铃铛的半径和条的粗细:

条的长度是startPointendPoint的距离:length = hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y)

为了简化为这个形状创建路径的过程,让我们以标准姿势绘制它,左铃在原点,条平行于 x 轴。在这个姿势中,以下是我们所知道的:

我们可以通过创建一个以原点为中心的圆弧来创建这个形状作为路径,连接到以(length, 0) 为中心的另一个(镜像)圆弧。要创建这些弧线,我们需要这个mysteryAngle

我们可以找出mysteryAngle 是否可以找到钟与杆相交的任何弧形端点。具体来说,我们会找到这个点的坐标:

我们对mysteryPoint 了解多少?我们知道它位于铃铛和酒吧顶部的交汇处。所以我们知道它与原点的距离为bellRadius,与x 轴的距离为barThickness / 2

所以我们立即知道mysteryPoint.y = barThickness / 2,我们可以使用勾股定理来计算mysteryPoint.x = sqrt(bellRadius² - mysteryPoint.y²)

找到mysteryPoint,我们可以使用我们选择的反三角函数计算mysteryAngle。反正弦,我选你! mysteryAngle = asin(mysteryPoint.y / bellRadius).

我们现在知道了以标准姿势创建路径所需的一切。要将其从标准姿势移动到所需姿势(从 startPointendPoint,还记得吗?),我们将应用仿射变换。变换将平移(移动)路径,使左钟以startPoint 为中心,并旋转路径使右钟以endPoint 结束。

在编写创建路径的代码时,我们要注意以下几点:

如果长度太短以至于铃铛重叠怎么办?我们应该通过调整mysteryAngle 来优雅地处理这个问题,这样铃铛就可以无缝连接,而不会在它们之间出现奇怪的“负条”。

如果bellRadius 小于barThickness / 2 怎么办?我们应该通过强制 bellRadius 至少为 barThickness / 2 来优雅地处理这个问题。

如果length 为零怎么办?我们需要避免被零除。

这是我创建路径的代码,处理所有这些情况:

extension CGPath 
    class func barbell(from start: CGPoint, to end: CGPoint, barThickness proposedBarThickness: CGFloat, bellRadius proposedBellRadius: CGFloat) -> CGPath 
        let barThickness = max(0, proposedBarThickness)
        let bellRadius = max(barThickness / 2, proposedBellRadius)

        let vector = CGPoint(x: end.x - start.x, y: end.y - start.y)
        let length = hypot(vector.x, vector.y)

        if length == 0 
            return CGPath(ellipseIn: CGRect(origin: start, size: .zero).insetBy(dx: -bellRadius, dy: -bellRadius), transform: nil)
        

        var yOffset = barThickness / 2
        var xOffset = sqrt(bellRadius * bellRadius - yOffset * yOffset)
        let halfLength = length / 2
        if xOffset > halfLength 
            xOffset = halfLength
            yOffset = sqrt(bellRadius * bellRadius - xOffset * xOffset)
        

        let jointRadians = asin(yOffset / bellRadius)
        let path = CGMutablePath()
        path.addArc(center: .zero, radius: bellRadius, startAngle: jointRadians, endAngle: -jointRadians, clockwise: false)
        path.addArc(center: CGPoint(x: length, y: 0), radius: bellRadius, startAngle: .pi + jointRadians, endAngle: .pi - jointRadians, clockwise: false)
        path.closeSubpath()

        let unitVector = CGPoint(x: vector.x / length, y: vector.y / length)
        var transform = CGAffineTransform(a: unitVector.x, b: unitVector.y, c: -unitVector.y, d: unitVector.x, tx: start.x, ty: start.y)
        return path.copy(using: &transform)!
    

一旦我们有了路径,我们需要用正确的颜色填充它,用正确的颜色和线宽对其进行描边,并在它周围画一个阴影。我在IDEInterfaceBuilderKit 上使用了Hopper Disassembler 来确定Xcode 的确切尺寸和颜色。 Xcode 将其全部绘制到自定义视图的drawRect: 中的图形上下文中,但我们将使我们的自定义视图使用CAShapeLayer。我们最终不会像 Xcode 一样精确地绘制阴影,但它已经足够接近了。

class ConnectionView: NSView 
    struct Parameters 
        var startPoint = CGPoint.zero
        var endPoint = CGPoint.zero
        var barThickness = CGFloat(2)
        var ballRadius = CGFloat(3)
    

    var parameters = Parameters()  didSet  needsLayout = true  

    override init(frame: CGRect) 
        super.init(frame: frame)
        commonInit()
    

    required init?(coder decoder: NSCoder) 
        super.init(coder: decoder)
        commonInit()
    

    let shapeLayer = CAShapeLayer()
    override func makeBackingLayer() -> CALayer  return shapeLayer 

    override func layout() 
        super.layout()

        shapeLayer.path = CGPath.barbell(from: parameters.startPoint, to: parameters.endPoint, barThickness: parameters.barThickness, bellRadius: parameters.ballRadius)
        shapeLayer.shadowPath = CGPath.barbell(from: parameters.startPoint, to: parameters.endPoint, barThickness: parameters.barThickness + shapeLayer.lineWidth / 2, bellRadius: parameters.ballRadius + shapeLayer.lineWidth / 2)
    

    private func commonInit() 
        wantsLayer = true

        shapeLayer.lineJoin = kCALineJoinMiter
        shapeLayer.lineWidth = 0.75
        shapeLayer.strokeColor = NSColor.white.cgColor
        shapeLayer.fillColor = NSColor(calibratedHue: 209/360, saturation: 0.83, brightness: 1, alpha: 1).cgColor
        shapeLayer.shadowColor = NSColor.selectedControlColor.blended(withFraction: 0.2, of: .black)?.withAlphaComponent(0.85).cgColor
        shapeLayer.shadowRadius = 3
        shapeLayer.shadowOpacity = 1
        shapeLayer.shadowOffset = .zero
    

我们可以在操场上测试它以确保它看起来不错:

import PlaygroundSupport

let view = NSView()
view.setFrameSize(CGSize(width: 400, height: 200))
view.wantsLayer = true
view.layer!.backgroundColor = NSColor.white.cgColor

PlaygroundPage.current.liveView = view

for i: CGFloat in stride(from: 0, through: 9, by: CGFloat(0.4)) 
    let connectionView = ConnectionView(frame: view.bounds)
    connectionView.parameters.startPoint = CGPoint(x: CGFloat(i) * 40 + 15, y: 50)
    connectionView.parameters.endPoint = CGPoint(x: CGFloat(i) * 40 + 15, y: 50 + CGFloat(i))
    view.addSubview(connectionView)


let connectionView = ConnectionView(frame: view.bounds)
connectionView.parameters.startPoint = CGPoint(x: 50, y: 100)
connectionView.parameters.endPoint = CGPoint(x: 350, y: 150)
view.addSubview(connectionView)

结果如下:

跨多个屏幕绘图

如果您的 Mac 连接了多个屏幕(显示器),并且如果您在系统偏好设置的任务控制面板中打开了“显示器具有单独的空间”(这是默认设置),则 macOS 不会让窗口跨越两个屏幕。这意味着您不能使用单个窗口来绘制跨多个监视器的连接线。如果您想让用户将一个窗口中的对象连接到另一个窗口中的对象,这很重要,就像 Xcode 所做的那样:

这是在我们的其他窗口顶部跨多个屏幕绘制线条的清单:

我们需要为每个屏幕创建一个窗口。 我们需要将每个窗口设置为填充其屏幕并完全透明且没有阴影。 我们需要将每个窗口的窗口级别设置为 1,以使其高于我们的普通窗口(窗口级别为 0)。 我们需要告诉每个窗口不要在关闭时释放自己,因为我们不喜欢神秘的自动释放池崩溃。 每个窗口都需要自己的ConnectionView。 为保持坐标系统一,我们将调整每个ConnectionViewbounds,使其坐标系与屏幕坐标系匹配。 我们会告诉每个ConnectionView 画出整条连接线;每个视图都会将其绘制的内容裁剪到自己的范围内。 这可能不会发生,但我们会安排在屏幕排列发生变化时收到通知。如果发生这种情况,我们将添加/删除/更新窗口以涵盖新的安排。

让我们创建一个类来封装所有这些细节。使用LineOverlay 的实例,我们可以根据需要更新连接的起点和终点,并在完成后从屏幕上移除覆盖。

class LineOverlay 

    init(startScreenPoint: CGPoint, endScreenPoint: CGPoint) 
        self.startScreenPoint = startScreenPoint
        self.endScreenPoint = endScreenPoint

        NotificationCenter.default.addObserver(self, selector: #selector(LineOverlay.screenLayoutDidChange(_:)), name: .NSApplicationDidChangeScreenParameters, object: nil)
        synchronizeWindowsToScreens()
    

    var startScreenPoint: CGPoint  didSet  setViewPoints()  

    var endScreenPoint: CGPoint  didSet  setViewPoints()  

    func removeFromScreen() 
        windows.forEach  $0.close() 
        windows.removeAll()
    

    private var windows = [NSWindow]()

    deinit 
        NotificationCenter.default.removeObserver(self)
        removeFromScreen()
    

    @objc private func screenLayoutDidChange(_ note: Notification) 
        synchronizeWindowsToScreens()
    

    private func synchronizeWindowsToScreens() 
        var spareWindows = windows
        windows.removeAll()
        for screen in NSScreen.screens() ?? [] 
            let window: NSWindow
            if let index = spareWindows.index(where:  $0.screen === screen) 
                window = spareWindows.remove(at: index)
             else 
                let styleMask = NSWindowStyleMask.borderless
                window = NSWindow(contentRect: .zero, styleMask: styleMask, backing: .buffered, defer: true, screen: screen)
                window.contentView = ConnectionView()
                window.isReleasedWhenClosed = false
                window.ignoresMouseEvents = true
            
            windows.append(window)
            window.setFrame(screen.frame, display: true)

            // Make the view's geometry match the screen geometry for simplicity.
            let view = window.contentView!
            var rect = view.bounds
            rect = view.convert(rect, to: nil)
            rect = window.convertToScreen(rect)
            view.bounds = rect

            window.backgroundColor = .clear
            window.isOpaque = false
            window.hasShadow = false
            window.isOneShot = true
            window.level = 1

            window.contentView?.needsLayout = true
            window.orderFront(nil)
        

        spareWindows.forEach  $0.close() 
    

    private func setViewPoints() 
        for window in windows 
            let view = window.contentView! as! ConnectionView
            view.parameters.startPoint = startScreenPoint
            view.parameters.endPoint = endScreenPoint
        
    


使用Cocoa拖拽找到拖拽目标并进行spring-loading

当用户拖动鼠标时,我们需要一种方法来找到连接的(潜在的)放置目标。支持弹簧加载也很好。

如果您不知道,弹簧加载是 macOS 的一项功能,如果您将拖动鼠标悬停在容器上片刻,macOS 将自动打开容器而不会中断拖动。例子:

如果您拖动到不是最前面的窗口,macOS 会将窗口置于最前面。 如果您拖到 Finder 文件夹图标上,Finder 将打开文件夹窗口让您拖到文件夹中的项目上。 如果您在 Safari 或 Chrome 中拖动到选项卡句柄(在窗口顶部),浏览器将选择该选项卡,让您将项目拖放到选项卡中。 如果您在 Xcode 中控制并拖动连接到情节提要或 xib 菜单栏中的菜单项,Xcode 将打开该项目的菜单。

如果我们使用标准的 Cocoa 拖放支持来跟踪拖放并找到放置目标,那么我们将“免费”获得弹簧加载支持。

为了支持标准的 Cocoa 拖放,我们需要在某个对象上实现 NSDraggingSource 协议,这样我们就可以从 拖拽一些东西,而在另一个对象上实现 NSDraggingDestination 协议,所以我们可以拖动的东西。我们将在一个名为ConnectionDragController 的类中实现NSDraggingSource,我们将在一个名为DragEndpoint 的自定义视图类中实现NSDraggingDestination

首先,让我们看一下DragEndpointNSView 的子类)。 NSView 已经符合 NSDraggingDestination,但并没有做太多。我们需要实现NSDraggingDestination协议的四种方法。拖动会话将调用这些方法来让我们知道拖动何时进入和离开目的地、拖动何时完全结束以及何时“执行”拖动(假设此目的地是拖动实际结束的位置)。我们还需要注册我们可以接受的拖拽数据的类型。

我们要注意两件事:

我们只想接受作为连接尝试的拖动。我们可以通过检查源是否是我们的自定义拖动源ConnectionDragController来判断拖动是否是连接尝试。 我们将使DragEndpoint 看起来是拖动源(仅在视觉上,不是以编程方式)。我们不想让用户将端点连接到自身,因此我们需要确保作为连接源的端点不能也用作连接的目标。我们将使用 state 属性来跟踪此端点是空闲的、充当源还是充当目标。

当用户最终在有效的放置目标上释放鼠标按钮时,拖动会话通过发送performDragOperation(_:) 使其成为“执行”拖动的目标。会话不会告诉拖放源最终发生的位置。但是我们可能想要做在源中建立连接(在我们的数据模型中)的工作。想一想它在 Xcode 中是如何工作的:当您从Main.storyboard 中的一个按钮拖动到ViewController.swift 并创建一个动作时,连接不会记录在拖动结束的ViewController.swift 中;它记录在Main.storyboard 中,作为按钮持久数据的一部分。因此,当拖动会话告诉目标“执行”拖动时,我们将使目标 (DragEndpoint) 将自身传递回拖动源上的 connect(to:) 方法,在那里可以进行真正的工作。

class DragEndpoint: NSView 

    enum State 
        case idle
        case source
        case target
    

    var state: State = State.idle  didSet  needsLayout = true  

    public override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation 
        guard case .idle = state else  return [] 
        guard (sender.draggingSource() as? ConnectionDragController)?.sourceEndpoint != nil else  return [] 
        state = .target
        return sender.draggingSourceOperationMask()
    

    public override func draggingExited(_ sender: NSDraggingInfo?) 
        guard case .target = state else  return 
        state = .idle
    

    public override func draggingEnded(_ sender: NSDraggingInfo?) 
        guard case .target = state else  return 
        state = .idle
    

    public override func performDragOperation(_ sender: NSDraggingInfo) -> Bool 
        guard let controller = sender.draggingSource() as? ConnectionDragController else  return false 
        controller.connect(to: self)
        return true
    

    override init(frame: NSRect) 
        super.init(frame: frame)
        commonInit()
    

    required init?(coder decoder: NSCoder) 
        super.init(coder: decoder)
        commonInit()
    

    private func commonInit() 
        wantsLayer = true
        register(forDraggedTypes: [kUTTypeData as String])
    

    // Drawing code omitted here but is in my github repo.

现在我们可以实现ConnectionDragController 作为拖动源并管理拖动会话和LineOverlay

要开始拖动会话,我们必须在视图上调用beginDraggingSession(with:event:source:);它将是发生鼠标按下事件的DragEndpoint。 会话在拖动实际开始、移动和结束时通知源。我们使用这些通知来创建和更新LineOverlay。 由于我们不提供任何图像作为NSDraggingItem 的一部分,因此会话不会绘制任何被拖动的内容。这很好。 默认情况下,如果拖动在有效目的地之外结束,会话将动画……什么都没有……回到拖动的开始,然后通知源拖动已经结束。在此动画中,线叠加层悬空,冻结。它看起来坏了。我们告诉会话不要动画回到开始以避免这种情况。

由于这只是一个演示,我们在connect(to:) 中连接端点所做的“工作”只是打印它们的描述。在实际应用中,您实际上会修改数据模型。

class ConnectionDragController: NSObject, NSDraggingSource 

    var sourceEndpoint: DragEndpoint?

    func connect(to target: DragEndpoint) 
        Swift.print("Connect \(sourceEndpoint!) to \(target)")
    

    func trackDrag(forMouseDownEvent mouseDownEvent: NSEvent, in sourceEndpoint: DragEndpoint) 
        self.sourceEndpoint = sourceEndpoint
        let item = NSDraggingItem(pasteboardWriter: NSPasteboardItem(pasteboardPropertyList: "\(view)", ofType: kUTTypeData as String)!)
        let session = sourceEndpoint.beginDraggingSession(with: [item], event: mouseDownEvent, source: self)
        session.animatesToStartingPositionsOnCancelOrFail = false
    

    func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation 
        switch context 
        case .withinApplication: return .generic
        case .outsideApplication: return []
        
    

    func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) 
        sourceEndpoint?.state = .source
        lineOverlay = LineOverlay(startScreenPoint: screenPoint, endScreenPoint: screenPoint)
    

    func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) 
        lineOverlay?.endScreenPoint = screenPoint
    

    func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) 
        lineOverlay?.removeFromScreen()
        sourceEndpoint?.state = .idle
    

    func ignoreModifierKeys(for session: NSDraggingSession) -> Bool  return true 

    private var lineOverlay: LineOverlay?


这就是你所需要的。提醒一下,您可以在此答案的顶部找到包含完整演示项目的 github 存储库的链接。

【讨论】:

这样一个惊人而鼓舞人心的答案。非常感谢! 这很棒。我克隆了存储库并尝试了该应用程序。在首次启动演示应用程序时,拖动不起作用,我在控制台中收到了CoreDragCreate 错误。 AskDifferent 上的这个答案解决了它:apple.stackexchange.com/questions/242111/… 很奇怪。我从来没有见过这样的事情。 现在我真的无法让它在NSOutlineView(行)的子视图上工作; hittest() 只是不会超出大纲视图本身... 您可能需要查看validateProposedFirstResponder:forEvent:【参考方案2】:

使用透明的 NSWindow :

var window: NSWindow!

func createLinePath(from: NSPoint, to: NSPoint) -> CGPath 
    let path = CGMutablePath()

    path.move(to: from)
    path.addLine(to: to)

    return path


override func viewDidLoad() 
    super.viewDidLoad()

    //Transparent window
    window = NSWindow()
    window.styleMask = .borderless
    window.backgroundColor = .clear
    window.isOpaque = false
    window.hasShadow = false

    //Line
    let line = CAShapeLayer()

    line.path = createLinePath(from: NSPoint(x: 0, y: 0), to: NSPoint(x: 100, y: 100))
    line.lineWidth = 10.0
    line.strokeColor = NSColor.blue.cgColor

    //Update
    NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) 
        let newPos = NSEvent.mouseLocation()

        line.path = self.createLinePath(from: NSPoint(x: 0, y: 0), to: newPos)

        return $0
    

    window.contentView!.layer = line
    window.contentView!.wantsLayer = true

    window.setFrame(NSScreen.main()!.frame, display: true)

    window.makeKeyAndOrderFront(nil)

【讨论】:

【参考方案3】:

尝试将Rob Mayoff's excellent solution above 应用到我自己的项目界面中,该界面基于NSOutlineView,我遇到了一些问题。如果它可以帮助任何尝试实现相同目标的人,我将在此答案中详细说明这些陷阱。

解决方案中提供的示例代码通过在视图控制器上实现mouseDown(with:)来检测拖动的开始,然后在窗口的内容视图上调用hittest()以获得@ 987654326@ 子视图(潜在的)拖动的来源。使用大纲视图时,这会导致两个陷阱,详见下一节。

1。鼠标按下事件

似乎当涉及到表格视图或大纲视图时,mouseDown(with:) 永远不会在视图控制器上被调用,我们需要在 大纲视图 本身中重写该方法。

2。命中测试

NSTableView - 并且通过扩展,NSOutlineView- 覆盖 NSResponder 方法 validateProposedFirstResponder(_:for:),这会导致 hittest() 方法失败:它总是返回大纲视图本身,以及所有子视图(包括我们的单元格内的目标DragEndpoint 子视图)仍然无法访问。

来自documentation:

表格中的视图或控件有时需要响应传入的 事件。确定一个特定的子视图是否应该接收 当前鼠标事件,一个表视图调用 validateProposedFirstResponder:forEvent: 在其实施中 hitTest。如果创建表视图子类,则可以覆盖 validateProposedFirstResponder:forEvent: 指定哪些视图可以 成为第一响应者。这样,您就会收到鼠标事件。

起初我尝试覆盖:

override func validateProposedFirstResponder(_ responder: NSResponder, for event: NSEvent?) -> Bool 
    if responder is DragEndpoint 
        return true
    
    return super.validateProposedFirstResponder(responder, for: event)

...它奏效了,但阅读文档进一步提出了一种更智能、侵入性更小的方法:

默认NSTableView实现 validateProposedFirstResponder:forEvent: 使用以下逻辑:

    为所有建议的第一响应者视图返回 YES,除非它们是 NSControl 的实例或子类。

    确定是否建议 第一响应者是NSControl 实例或子类。如果控制 是一个NSButton 对象,返回YES。如果控件不是NSButton, 调用控件的hitTestForEvent:inRect:ofView:查看是否 命中区域是可跟踪的(即NSCellHitTrackableArea)或者是 可编辑文本区域(即NSCellHitEditableTextArea),然后返回 适当的值。请注意,如果点击文本区域,NSTableView 也会延迟第一响应者的行动。

(强调我的)

...这很奇怪,因为感觉它应该说:

    为所有建议的第一响应者视图返回 NO,除非它们是 NSControl 的实例或子类。

,但无论如何,我修改了 Rob 的代码,使 DragEndpoint 成为 NSControl 的子类(不仅仅是 NSView),这也有效。

3。管理拖动会话

因为NSOutlineView 仅通过其数据源协议公开了有限数量的拖放事件(并且拖动会话本身不能从数据源端进行有意义的修改),它除非我们子类化大纲视图并覆盖NSDraggingSource 方法,否则似乎不可能完全控制拖动会话。 只有在大纲视图本身覆盖draggingSession(_:willBeginAt:),我们才能防止调用超类实现并开始实际的项目拖动(显示拖动的行图像)。

我们可以从DragEndpoint 子视图的mouseDown(with:) 方法开始一个单独的拖动会话:当实现时,它在大纲视图上被调用之前相同的方法(这又是什么触发拖动会话开始)。但是,如果我们将拖动会话从大纲视图中移开,当拖动到可扩展项目上方时,似乎不可能“免费”进行 springloading

因此,我放弃了ConnectionDragController 类并将其所有逻辑移至大纲视图子类:tackDrag() 方法、活动的DragEndpoint 属性以及NSDraggingSource 协议的所有方法到大纲视图中.

理想情况下,我希望避免子类化NSOutlineView(不鼓励),而是更干净地实现此行为,仅通过大纲视图的委托/数据源和/或外部类(如原始ConnectionDragController) ,但似乎是不可能的。

我还没有让弹簧加载部分工作(它正在工作,但现在没有,所以我仍在研究它......)。


我也做了一个示例项目,但我仍在修复小问题。准备好后,我将发布 GiHub 存储库的链接。

【讨论】:

以上是关于如何重现这条 Xcode 蓝色拖线的主要内容,如果未能解决你的问题,请参考以下文章

xcode内存调试器中的蓝色和绿色是啥意思

如何删除选中时覆盖 UITabBarItem 的蓝色方块?

SwiftUI 视图以蓝色背景显示

Xcode使用xib拖线时出现: could not insert new outlet connection

如何在没有蓝色的所有感觉颜色的情况下将 QUADS 着色为蓝色

iOS-iOS 获取蓝色文件夹图片