Удаление разделов и заголовка в NSCollectionView (разметка закрепленного заголовка) - PullRequest
1 голос
/ 09 февраля 2020

С кодом, размещенным ниже для макета представления коллекции липких заголовков, как бы я go об удалении раздела (вместе с заголовком) из представления, когда я удалил последний элемент в разделе? Не уверен, есть ли место в файле 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
  }

}

Image Loader

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)
  }


}

Схема потока Sticky Headers

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
  }

}
...