3

是否可以通过拖动子视图来更改NSStackView中的视图顺序,就像我们在 NSTableView 中所做的那样?

4

2 回答 2

6

这是一个 NSStackView 子类的实现,其内容可以通过拖动重新排序:

//
//  DraggingStackView.swift
//  Analysis
//
//  Created by Mark Onyschuk on 2017-02-02.
//  Copyright © 2017 Mark Onyschuk. All rights reserved.
//
import Cocoa

class DraggingStackView: NSStackView {

    var isEnabled = true

    // MARK: -
    // MARK: Update Function
    var update: (NSStackView, Array<NSView>)->Void = { stack, views in
        stack.views.forEach {
            stack.removeView($0)
        }

        views.forEach {
            stack.addView($0, in: .leading)

            switch stack.orientation {
            case .horizontal:
                $0.topAnchor.constraint(equalTo: stack.topAnchor).isActive = true
                $0.bottomAnchor.constraint(equalTo: stack.bottomAnchor).isActive = true

            case .vertical:
                $0.leadingAnchor.constraint(equalTo: stack.leadingAnchor).isActive = true
                $0.trailingAnchor.constraint(equalTo: stack.trailingAnchor).isActive = true
            }
        }
    }


    // MARK: -
    // MARK: Event Handling
    override func mouseDragged(with event: NSEvent) {
        if isEnabled {
            let location = convert(event.locationInWindow, from: nil)
            if let dragged = views.first(where: { $0.hitTest(location) != nil }) {
                reorder(view: dragged, event: event)
            }
        } else {
            super.mouseDragged(with: event)
        }

    }

    private func reorder(view: NSView, event: NSEvent) {
        guard let layer = self.layer else { return }
        guard let cached = try? self.cacheViews() else { return }

        let container = CALayer()
        container.frame = layer.bounds
        container.zPosition = 1
        container.backgroundColor = NSColor.underPageBackgroundColor.cgColor

        cached
            .filter  { $0.view !== view }
            .forEach { container.addSublayer($0) }

        layer.addSublayer(container)
        defer { container.removeFromSuperlayer() }

        let dragged = cached.first(where: { $0.view === view })!

        dragged.zPosition = 2
        layer.addSublayer(dragged)
        defer { dragged.removeFromSuperlayer() }

        let d0 = view.frame.origin
        let p0 = convert(event.locationInWindow, from: nil)

        window!.trackEvents(matching: [.leftMouseDragged, .leftMouseUp], timeout: 1e6, mode: .eventTrackingRunLoopMode) { event, stop in
            if event.type == .leftMouseDragged {
                let p1 = self.convert(event.locationInWindow, from: nil)

                let dx = (self.orientation == .horizontal) ? p1.x - p0.x : 0
                let dy = (self.orientation == .vertical)   ? p1.y - p0.y : 0

                CATransaction.begin()
                CATransaction.setDisableActions(true)
                dragged.frame.origin.x = d0.x + dx
                dragged.frame.origin.y = d0.y + dy
                CATransaction.commit()

                let reordered = self.views.map {
                    (view: $0,
                     position: $0 !== view
                        ? NSPoint(x: $0.frame.midX, y: $0.frame.midY)
                        : NSPoint(x: dragged.frame.midX, y: dragged.frame.midY))
                    }
                    .sorted {
                        switch self.orientation {
                        case .vertical: return $0.position.y < $1.position.y
                        case .horizontal: return $0.position.x < $1.position.x
                        }
                    }
                    .map { $0.view }

                let nextIndex = reordered.index(of: view)!
                let prevIndex = self.views.index(of: view)!

                if nextIndex != prevIndex {
                    self.update(self, reordered)
                    self.layoutSubtreeIfNeeded()

                    CATransaction.begin()
                    CATransaction.setAnimationDuration(0.15)
                    CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut))

                    for layer in cached {
                        layer.position = NSPoint(x: layer.view.frame.midX, y: layer.view.frame.midY)
                    }

                    CATransaction.commit()
                }

            } else {
                view.mouseUp(with: event)                    
                stop.pointee = true
            }
        }
    }

    // MARK: -
    // MARK: View Caching
    private class CachedViewLayer: CALayer {
        let view: NSView!

        enum CacheError: Error {
            case bitmapCreationFailed
        }

        override init(layer: Any) {
            self.view = (layer as! CachedViewLayer).view
            super.init(layer: layer)
        }

        init(view: NSView) throws {
            self.view = view

            super.init()

            guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { throw CacheError.bitmapCreationFailed }
            view.cacheDisplay(in: view.bounds, to: bitmap)

            frame = view.frame
            contents = bitmap.cgImage
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }

    private func cacheViews() throws -> [CachedViewLayer] {
        return try views.map { try cacheView(view: $0) }
    }

    private func cacheView(view: NSView) throws -> CachedViewLayer {
        return try CachedViewLayer(view: view)
    }
}

该代码要求您的堆栈支持图层,并在拖动处理期间使用子图层来模拟和动画其内容视图。通过覆盖 mouseDragged(with:) 检测到拖动,因此如果堆栈的内容消耗此事件,则不会启动。

于 2017-02-03T12:30:59.167 回答
1

没有对重新排序 NSStackView 子视图的内置支持。

于 2016-01-15T01:37:34.023 回答