使用下面发布的粘性标题集合视图布局的代码,当我删除一个部分中的最后一项时,我将如何从视图中删除一个部分(连同标题)?不确定 ViewController 文件中是否有我可以执行此操作的位置,或者可能是 ImageLoader 文件?
View Controller
import Cocoa
class ViewController: NSViewController {
@IBOutlet weak var collectionView: NSCollectionView!
@IBOutlet weak var addSlideButton: NSButton!
@IBOutlet weak var removeSlideButton: NSButton!
var indexPathsOfItemsBeingDragged: Set<NSIndexPath>!
let imageDirectoryLoader = ImageDirectoryLoader()
override func viewDidLoad() {
super.viewDidLoad()
let initialFolderUrl = URL(fileURLWithPath: "/Library/Desktop Pictures", isDirectory: true)
imageDirectoryLoader.loadDataForFolderWithUrl(initialFolderUrl)
configureCollectionView()
registerForDragAndDrop()
}
func loadDataForNewFolderWithUrl(_ folderURL: URL) {
imageDirectoryLoader.loadDataForFolderWithUrl(folderURL)
collectionView.reloadData()
}
fileprivate func configureCollectionView() {
let flowLayout = StickyHeadersCollectionViewFlowLayout()
flowLayout.itemSize = NSSize(width: 160.0, height: 140.0)
flowLayout.sectionInset = NSEdgeInsets(top: 10.0, left: 20, bottom: 10.0, right: 20.0)
flowLayout.minimumInteritemSpacing = 20.0
flowLayout.minimumLineSpacing = 20.0
//flowLayout.sectionHeadersPinToVisibleBounds = true
collectionView.collectionViewLayout = flowLayout
view.wantsLayer = true
collectionView.layer?.backgroundColor = NSColor.white.cgColor
}
@IBAction func showHideSections(_ sender: AnyObject) {
let show = (sender as! NSButton).state
imageDirectoryLoader.singleSectionMode = (show == NSControl.StateValue.off)
imageDirectoryLoader.setupDataForUrls(nil)
collectionView.reloadData()
}
func highlightItems(_ selected: Bool, atIndexPaths: Set<IndexPath>) {
for indexPath in atIndexPaths {
guard let item = collectionView.item(at: indexPath) else {continue}
(item as! CollectionViewItem).setHighlight(selected)
}
addSlideButton.isEnabled = collectionView.selectionIndexPaths.count == 1
removeSlideButton.isEnabled = !collectionView.selectionIndexPaths.isEmpty
}
private func inserAtIndexPathFromURLs (urls: [NSURL], atIndexPath: NSIndexPath) {
var indexPaths: Set<IndexPath> = []
let section = atIndexPath.section
var currentItem = atIndexPath.item
for url in urls {
let imageFile = ImageFile(url: url as URL)
let currentIndexPath = NSIndexPath(forItem: currentItem, inSection: section)
imageDirectoryLoader.insertImage(image: imageFile, atIndexPath: currentIndexPath)
indexPaths.insert(currentIndexPath as IndexPath)
currentItem += 1
}
collectionView.insertItems(at: indexPaths)
}
@IBAction func addSlide(_ sender: NSButton) {
let insertAtIndexPath = collectionView.selectionIndexPaths.first!
let openPanel = NSOpenPanel()
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.allowsMultipleSelection = true
openPanel.allowedFileTypes = ["public.image"]
openPanel.beginSheetModal(for: self.view.window!) { (response) in
guard response.rawValue == NSFileHandlingPanelOKButton
else {
return
}
self.inserAtIndexPathFromURLs(urls: openPanel.urls as [NSURL], atIndexPath: insertAtIndexPath as NSIndexPath)
}
}
@IBAction func removeSlide(_ sender: NSButton) {
let selectionIndexPaths = collectionView.selectionIndexPaths
if selectionIndexPaths.isEmpty {
return
}
var selectionArray = Array(selectionIndexPaths)
selectionArray.sort{path1, path2 in return path1.compare(path2) == .orderedDescending}
for itemIndexPath in selectionArray {
imageDirectoryLoader.removeImageAtIndexPath(indexPath: itemIndexPath as NSIndexPath)
}
NSAnimationContext.current.duration = 1.0
collectionView.animator().deleteItems(at: selectionIndexPaths)
}
func registerForDragAndDrop() {
collectionView.registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeURL as String)])
collectionView.setDraggingSourceOperationMask(.every, forLocal: true)
collectionView.setDraggingSourceOperationMask(.every, forLocal: false)
}
}
// MARK: - NSCollectionViewDataSource
extension ViewController : NSCollectionViewDataSource {
func numberOfSections(in collectionView: NSCollectionView) -> Int {
return imageDirectoryLoader.numberOfSections
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return imageDirectoryLoader.numberOfItemsInSection(section)
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CollectionViewItem"), for: indexPath)
guard let collectionViewItem = item as? CollectionViewItem
else {return item}
let imageFile = imageDirectoryLoader.imageFileForIndexPath(indexPath)
collectionViewItem.imageFile = imageFile
let isItemSelected = collectionView.selectionIndexPaths.contains(indexPath)
collectionViewItem.setHighlight(isItemSelected)
return item
}
func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> NSView {
let identifier:String = kind == NSCollectionView.elementKindSectionHeader ? "HeaderView" : ""
let view = collectionView.makeSupplementaryView(ofKind: kind, withIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier), for: indexPath)
if kind == NSCollectionView.elementKindSectionHeader {
let headerView = view as! HeaderView
headerView.sectionTitle.stringValue = "Section \(indexPath.section)"
let numberOfItemsInSection = imageDirectoryLoader.numberOfItemsInSection(indexPath.section)
headerView.imageCount.stringValue = "\(numberOfItemsInSection) image files"
}
return view
}
}
// MARK: - NSCollectionViewDelegateFlowLayout
extension ViewController : NSCollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> NSSize {
return imageDirectoryLoader.singleSectionMode ? NSZeroSize : NSSize(width: 1000, height: 40)
}
}
// MARK: - NSCollectionViewDelegate
extension ViewController : NSCollectionViewDelegate {
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
highlightItems(true, atIndexPaths: indexPaths)
}
func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set<IndexPath>) {
highlightItems(false, atIndexPaths: indexPaths)
}
func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexes: IndexSet, with event: NSEvent) -> Bool {
return true
}
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
let imageFile = imageDirectoryLoader.imageFileForIndexPath(indexPath)
return imageFile.url.absoluteURL as NSPasteboardWriting
}
func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
indexPathsOfItemsBeingDragged = indexPaths as Set<NSIndexPath>
}
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation {
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
proposedDropOperation.pointee = NSCollectionView.DropOperation.before
}
if indexPathsOfItemsBeingDragged == nil {
return NSDragOperation.copy
}
else {
let sectionOfItemBeingbeinDragged = indexPathsOfItemsBeingDragged.first!.section
let proposedDropSection = proposedDropIndexPath.pointee.section
if
sectionOfItemBeingbeinDragged == proposedDropSection && indexPathsOfItemsBeingDragged.count == 1 {
return NSDragOperation.move
}
else {
return NSDragOperation()
}
}
}
func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool {
if indexPathsOfItemsBeingDragged != nil {
let indexPathOfFirstItemBeingDragged = indexPathsOfItemsBeingDragged.first!
var toIndexPath: NSIndexPath
if indexPathOfFirstItemBeingDragged.compare(indexPath) == .orderedAscending {
toIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
}
else {
toIndexPath = NSIndexPath(forItem: indexPath.item, inSection: indexPath.section)
}
imageDirectoryLoader.moveImageFromIndexPath(indexPath: indexPathOfFirstItemBeingDragged, toInexPath: toIndexPath)
NSAnimationContext.current.duration = 0.5
collectionView.animator().moveItem(at: indexPathOfFirstItemBeingDragged as IndexPath, to: toIndexPath as IndexPath)
}
else {
var droppedObjects = Array<NSURL>()
draggingInfo.enumerateDraggingItems(options: .concurrent, for: collectionView, classes: [NSURL.self], searchOptions: [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly: NSNumber(value: true)]) { (draggingItem, idx, stop) in
if let url = draggingItem.item as? NSURL {
droppedObjects.append(url)
}
}
inserAtIndexPathFromURLs(urls: droppedObjects, atIndexPath: indexPath as NSIndexPath)
}
return true
}
func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
indexPathsOfItemsBeingDragged = nil
}
}
图像加载器
import Cocoa
class ImageDirectoryLoader: NSObject {
fileprivate var imageFiles = [ImageFile]()
fileprivate(set) var numberOfSections = 1 // Read by ViewController
var singleSectionMode = false // Read/Write by ViewController
fileprivate struct SectionAttributes {
var sectionOffset: Int // the index of the first image of this section in the imageFiles array
var sectionLength: Int // number of images in the section
}
// sectionLengthArray - An array of randomly picked integers just for demo purposes.
// sectionLengthArray[0] is 7, i.e. put the first 7 images from the imageFiles array into section 0
// sectionLengthArray[1] is 3, i.e. put the next 3 images from the imageFiles array into section 1
// and so on...
fileprivate var sectionLengthArray = [7, 3, 2, 4, 11, 7, 10, 12, 20, 25, 10, 3, 30, 25, 40]
fileprivate var sectionsAttributesArray = [SectionAttributes]()
func setupDataForUrls(_ urls: [URL]?) {
if let urls = urls { // When new folder
createImageFilesForUrls(urls)
}
if sectionsAttributesArray.count > 0 { // If not first time, clean old sectionsAttributesArray
sectionsAttributesArray.removeAll()
}
numberOfSections = 1
if singleSectionMode {
setupDataForSingleSectionMode()
} else {
setupDataForMultiSectionMode()
}
}
fileprivate func setupDataForSingleSectionMode() {
let sectionAttributes = SectionAttributes(sectionOffset: 0, sectionLength: imageFiles.count)
sectionsAttributesArray.append(sectionAttributes) // sets up attributes for first section
}
fileprivate func setupDataForMultiSectionMode() {
let haveOneSection = singleSectionMode || sectionLengthArray.count < 2 || imageFiles.count <= sectionLengthArray[0]
var realSectionLength = haveOneSection ? imageFiles.count : sectionLengthArray[0]
var sectionAttributes = SectionAttributes(sectionOffset: 0, sectionLength: realSectionLength)
sectionsAttributesArray.append(sectionAttributes) // sets up attributes for first section
guard !haveOneSection else {return}
var offset: Int
var nextOffset: Int
let maxNumberOfSections = sectionLengthArray.count
for i in 1..<maxNumberOfSections {
numberOfSections += 1
offset = sectionsAttributesArray[i-1].sectionOffset + sectionsAttributesArray[i-1].sectionLength
nextOffset = offset + sectionLengthArray[i]
if imageFiles.count <= nextOffset {
realSectionLength = imageFiles.count - offset
nextOffset = -1 // signal this is last section for this collection
} else {
realSectionLength = sectionLengthArray[i]
}
sectionAttributes = SectionAttributes(sectionOffset: offset, sectionLength: realSectionLength)
sectionsAttributesArray.append(sectionAttributes)
if nextOffset < 0 {
break
}
}
}
fileprivate func createImageFilesForUrls(_ urls: [URL]) {
if imageFiles.count > 0 { // When not initial folder folder
imageFiles.removeAll()
}
for url in urls {
let imageFile = ImageFile(url: url)
imageFiles.append(imageFile)
}
}
fileprivate func getFilesURLFromFolder(_ folderURL: URL) -> [URL]? {
let options: FileManager.DirectoryEnumerationOptions =
[.skipsHiddenFiles, .skipsSubdirectoryDescendants, .skipsPackageDescendants]
let fileManager = FileManager.default
let resourceValueKeys = [URLResourceKey.isRegularFileKey, URLResourceKey.typeIdentifierKey]
guard let directoryEnumerator = fileManager.enumerator(at: folderURL, includingPropertiesForKeys: resourceValueKeys,
options: options, errorHandler: { url, error in
print("`directoryEnumerator` error: \(error).")
return true
}) else { return nil }
var urls: [URL] = []
for case let url as URL in directoryEnumerator {
do {
let resourceValues = try (url as NSURL).resourceValues(forKeys: resourceValueKeys)
guard let isRegularFileResourceValue = resourceValues[URLResourceKey.isRegularFileKey] as? NSNumber else { continue }
guard isRegularFileResourceValue.boolValue else { continue }
guard let fileType = resourceValues[URLResourceKey.typeIdentifierKey] as? String else { continue }
guard UTTypeConformsTo(fileType as CFString, "public.image" as CFString) else { continue }
urls.append(url)
}
catch {
print("Unexpected error occured: \(error).")
}
}
return urls
}
func numberOfItemsInSection(_ section: Int) -> Int {
return sectionsAttributesArray[section].sectionLength
}
func imageFileForIndexPath(_ indexPath: IndexPath) -> ImageFile {
let imageIndexInImageFiles = sectionsAttributesArray[indexPath.section].sectionOffset + indexPath.item
let imageFile = imageFiles[imageIndexInImageFiles]
return imageFile
}
func loadDataForFolderWithUrl(_ folderURL: URL) {
let urls = getFilesURLFromFolder(folderURL)
setupDataForUrls(urls)
}
func insertImage(image: ImageFile, atIndexPath: NSIndexPath) {
let imageIndexInImageFiles = sectionsAttributesArray[atIndexPath.section].sectionOffset + atIndexPath.item
imageFiles.insert(image, at: imageIndexInImageFiles)
let sectionToUpdate = atIndexPath.section
sectionsAttributesArray[sectionToUpdate].sectionLength += 1
if sectionLengthArray[sectionToUpdate] < numberOfSections - 1 {
for i in sectionToUpdate + 1...numberOfSections - 1 {
sectionsAttributesArray[i].sectionOffset += 1
}
}
}
func removeImageAtIndexPath(indexPath: NSIndexPath) -> ImageFile {
let imageIndexInImageFiles = sectionsAttributesArray[indexPath.section].sectionOffset + indexPath.item
let imageFileRemoved = imageFiles.remove(at: imageIndexInImageFiles)
let sectionToUpdate = indexPath.section
sectionsAttributesArray[sectionToUpdate].sectionLength -= 1
if sectionToUpdate < numberOfSections - 1 {
for i in sectionToUpdate + 1...numberOfSections - 1 {
sectionsAttributesArray[i].sectionOffset -= 1
}
}
return imageFileRemoved
}
func moveImageFromIndexPath(indexPath: NSIndexPath, toInexPath: NSIndexPath) {
let itemBeingDragged = removeImageAtIndexPath(indexPath: indexPath)
let destinationIsLower = indexPath.compare(toInexPath as IndexPath) == .orderedDescending
var indexPathOfDestination: NSIndexPath
if destinationIsLower {
indexPathOfDestination = toInexPath
}
else {
indexPathOfDestination = NSIndexPath(forItem: toInexPath.item - 1, inSection: toInexPath.section)
}
insertImage(image: itemBeingDragged, atIndexPath: indexPathOfDestination)
}
}
粘性标题流布局
import Cocoa
class StickyHeadersCollectionViewFlowLayout: NSCollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
// Helpers
let sectionsToAdd = NSMutableIndexSet()
var newLayoutAttributes = [NSCollectionViewLayoutAttributes]()
for layoutAttributesSet in layoutAttributes {
if layoutAttributesSet.representedElementCategory == .item {
// Add Layout Attributes
newLayoutAttributes.append(layoutAttributesSet)
// Update Sections to Add
sectionsToAdd.add(layoutAttributesSet.indexPath!.section)
}
else if layoutAttributesSet.representedElementCategory == .supplementaryView {
// Update Sections to Add
sectionsToAdd.add(layoutAttributesSet.indexPath!.section)
}
}
for section in sectionsToAdd {
let indexPath = IndexPath(item: 0, section: section)
if let sectionAttributes = self.layoutAttributesForSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, at: indexPath) {
newLayoutAttributes.append(sectionAttributes)
}
}
return newLayoutAttributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes? {
guard let layoutAttributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
else { return nil }
guard let boundaries = boundaries(forSection: indexPath.section)
else { return layoutAttributes }
guard let collectionView = collectionView
else { return layoutAttributes }
// Helpers
let contentOffsetY = CGFloat((collectionView.enclosingScrollView?.documentVisibleRect.origin.y)!)
var frameForSupplementaryView = layoutAttributes.frame
let minimum = boundaries.minimum - frameForSupplementaryView.height
let maximum = boundaries.maximum - frameForSupplementaryView.height
if contentOffsetY < minimum {
frameForSupplementaryView.origin.y = minimum
}
else if contentOffsetY > maximum {
frameForSupplementaryView.origin.y = maximum
}
else {
frameForSupplementaryView.origin.y = contentOffsetY
}
layoutAttributes.frame = frameForSupplementaryView
return layoutAttributes
}
func boundaries(forSection section: Int) -> (minimum: CGFloat, maximum: CGFloat)? {
// Helpers
var result = (minimum: CGFloat(0.0), maximum: CGFloat(0.0))
// Exit Early
guard let collectionView = collectionView
else { return result }
// Fetch Number of Items for Section
let numberOfItems = collectionView.numberOfItems(inSection: section)
// Exit Early
guard numberOfItems > 0
else { return result }
if let firstItem = layoutAttributesForItem(at: IndexPath(item: 0, section: section)),
let lastItem = layoutAttributesForItem(at: IndexPath(item: (numberOfItems - 1), section: section)) {
result.minimum = firstItem.frame.minY
result.maximum = lastItem.frame.maxY
// Take Header Size Into Account
//result.minimum -= headerReferenceSize.height
//result.maximum -= headerReferenceSize.height
// Take Section Inset Into Account
result.minimum -= sectionInset.top
//result.maximum += (sectionInset.top + sectionInset.bottom)
result.maximum += (sectionInset.bottom)
}
return result
}
}