Group Group Group Group Group Group Group Group Group
raywenderlich.com Forums

Advanced Collection Views in OS X Tutorial

If you want to learn about the advanced capabilities of NSCollectionView, you’ve come to the right place. This is the second part of a tutorial that covered the basics, and in this Advanced Collection Views in OS X Tutorial, you step deeper into the encompassing world of collection views. In this OS X tutorial, you’ll […]


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/1047-advanced-collection-views-in-os-x-tutorial

Thank you, I used some of your codes for my video joiner,
but with Swift 3 update, here // (0) set to Allow DRAG
func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexes: IndexSet, with event: NSEvent) -> Bool {
return true
}

//  (1)  First Event:  ready and copy Url to Pasteboard
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
    //NSLog("%@", " pasteboardWriterForItemAt  ")
    let thisFile: VideoFile = contents.object(at: indexPath.item) as! VideoFile
    return thisFile.videoUrl.absoluteURL as NSPasteboardWriting?
}


//  (2)  Second Event:  Get the Dragged Item Index Path
func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
    draggedItemsIndexPathSet = indexPaths
    //NSLog("%@", " willBeginAt  ")
}


//  (3)  third Event, continue firing,  until mouseUp (only single selection)
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionViewDropOperation>) -> NSDragOperation {
    
    //NSLog("%@", " validateDrop  ")
    
    if proposedDropOperation.pointee == NSCollectionViewDropOperation.on {
        proposedDropOperation.pointee = NSCollectionViewDropOperation.before
    }
    if draggedItemsIndexPathSet == nil {
        return NSDragOperation.copy
    } else {
        
        if  draggedItemsIndexPathSet.count == 1 {
            return NSDragOperation.move
        } else {
            return NSDragOperation()
        }
    }
}


//  (4)  After MouseUp
func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionViewDropOperation) -> Bool {
    
    // NSLog("%@", " acceptDrop  ")
    
    if draggedItemsIndexPathSet != nil {

        let firstIndex = draggedItemsIndexPathSet.first!
        var toIndexPath: IndexPath
        
        if (firstIndex as NSIndexPath).compare(indexPath) == .orderedAscending {
            toIndexPath = IndexPath(item: indexPath.item-1, section: indexPath.section)
        } else {
            toIndexPath = IndexPath(item: indexPath.item, section: indexPath.section)
        }

        // mirror contents move  imageDirectoryLoader.moveImageFromIndexPath(firstIndex, toIndexPath: toIndexPath)

        NSAnimationContext.current().duration = 0.50
        collectionView.animator().moveItem(at: firstIndex, to: toIndexPath)
   
    } else {
        // Assume Drop Source is From Finder and may be more than 1 file
        var droppedUrls = Array<URL>()
        draggingInfo.enumerateDraggingItems(options: NSDraggingItemEnumerationOptions.concurrent,
                                                for: collectionView,
                                            classes: [URL.self as AnyObject as! AnyObject.Type ],
                                      searchOptions: [NSPasteboardURLReadingFileURLsOnlyKey : NSNumber(value: true as Bool)],
                                              using: {(draggingItem, idx, stop) in
                                                if let url = draggingItem.item as? URL {
                                                    droppedUrls.append(url)
                                                }
        })

        //  droppedUrls (to be filtered as MovMp4M4v) and Insert Point: indexPath)
    
    }
    return true
}

// (5)  complete the Move, clear last dragging item index Path

func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
    
    draggedItemsIndexPathSet = nil
    
    // NSLog("%@", " endedAt  ")
}

Thank you for the awesome tutorials, they have helped me learn a lot. I was wondering though if you can point me somewhere that goes into cross section dragging in a collection view. I have tried, read, and tested, but have not been able to get the collection view to accept a drag and drop between sections. I get the indicator between images, but it just bounces back, its like the move item call just doesnt work if the sections are different.

Any help would be appreciated, Its killing me! LOL

I downloaded the final project and built it with Xcode 8.2.1 (8C1002). However, the application is not accepting drop operations. It allows a drag to start from within the collection view. I can drag items out to Finder. However, I can drag into the application, whether it started from within the application or not. Any ideas?

The tutorial blocks on purpose the drag and drop between sections. This was done in order to avoid adding all the logic necessary to support to sync the move operation by the collection view with the backing storage (ImageDirectoryLoader). To remove this restriction in the tutorial’s code go to the validateDrop method of the NSCollectionViewDelegate extension in ViewController and remove the condition “sectionOfItembeingDragged == proposedDropsection” from the if statement.
Mind you, this will introduce some bugs (image will move to the relevant section but with possible shift from the expected index inside the new section), because as I said above the move must be synced with the backing storage, but essentially the drag and drop between sections is working.

It would be most confusing to deal with 2 different builds (meaning the build related to your first post vs. the second post - build 8.2.1), so let’s focus on your latest build only.

You do not specify whether you converted to Swift 2.3 or Swift 3. Basically I checked again with both Swift versions and found everything to work as expected, as opposed to what you describe. If you converted to Swift 3, it is very much possible that some issues were introduced with the conversion (even though your project show no build errors).

In order to try to help you I would need the following info:

  1. Swift version.
  2. If Swift 3, please post the whole NSCollectionViewDelegate extension after conversion.
  3. Please be more specific in your description. You said for example: “the application is not accepting drop operations. It allows a drag to start from within the collection view. I can drag items out to Finder.”
    Instead I would like descriptions like this:

Drag source: app
Drag destination: app
Sections (when relevant): source section same/different as/from destination section
Result: Failure
Nature of failure: Crash/Image bounced back/else

or

Drag source: app
Drag destination: Finder
result: Success

etc.

Thank you for your response. I have converted to Swift 3 and I have modified things heavily. But to know that it is possible (I have read that there is a bug in the collection view control) is a help in itself. I will keep going and see if I can get it, would rather work it out than be told exactly what to do.

I never implemented the section restrictions, but I don’t think I have done the sync in the background data correctly. I am not using the Directory Finder anymore, I have moved over to a core data setup. I have most likely done something wrong there.

Once again, thank you for your advice.

Hi,
Just wanted to confirm that I was not updating the data in the background correctly, once I moved the data around right, I had no issues. Drag and drop working as expected. Thank you very much for your advice, your awesome tutorials, and for an awesome site.

Cheers!

I downloaded a new copy and built it again. Xcode insists on converting it to Swift 3 for reasons I don’t understand. However, this time I was a bit more careful about the conversion and almost everything seems to work. The only problem I’m having is dragging items into the application. When I drag an image into the application from Finder, the application fails with the following:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

Thanks for the tutorial, is there a way to fix the app so when you move the window you can still drag and drop? I verified it in the finished app as well as my implementation, once the main window is moved the drag and drop functions start to break down or don’t work at all.

I’m also suffering from a “Thread1:EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP,subcode=0x0” when I try to drag a photo from the finder into the collection view or out of the Collection View into the finder.
Operations:
Scenario 1
Drag source: app
Drag destination: app
Sections: remain in same section. Section 1 to section 1.
Result: Failure
Nature of failure: Crash EXC_BAD_INSTRUCTION

Scenario 2
Drag source: app
Drag destination: Finder
result: Failure
Nature of failure: Crash EXC_BAD_INSTRUCTION

Scenario 3
Drag source: Finder
Drag destination: app
result: Failure
Nature of failure: Crash EXC_BAD_INSTRUCTION

Details:

  1. Swift version: 3.1

  2. Here is my NSCollectionViewDelegate extension after Swift 3.0 conversion, after I attempted the drag and drop section but before implementing the drop target (from the tutorial dated July 19, 2016)

    // 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: “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 view = collectionView.makeSupplementaryView(ofKind: NSCollectionElementKindSectionHeader, withIdentifier: “HeaderView”, for: indexPath) as! HeaderView
    view.sectionTitle.stringValue = “Section (indexPath.section)”
    let numberOfItemsInSection = imageDirectoryLoader.numberOfItemsInSection(indexPath.section)
    view.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) {
    highlightItems(true, atIndexPaths: indexPaths)
    }

    func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) {
    highlightItems(false, atIndexPaths: indexPaths)
    }

    // 1
    func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexes: IndexSet, with event: NSEvent) -> Bool {
    return true
    }
    // 2
    func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
    let imageFile = imageDirectoryLoader.imageFileForIndexPath(indexPath)
    return imageFile.url?.absoluteURL as! NSPasteboardWriting //This line has the EXC_BAD_INSTRUCTION
    }
    }

//For reference here’s my ImageView since I’m having trouble unwrapping url.

class ImageFile {
  
  fileprivate(set) var thumbnail: NSImage?
  fileprivate(set) var fileName: String
  fileprivate(set) var url: URL?
  
  init?(url: URL) {
    fileName = url.lastPathComponent
    thumbnail = nil
    let imageSource = CGImageSourceCreateWithURL(url.absoluteURL as CFURL, nil)
    if let imageSource = imageSource {
      guard CGImageSourceGetType(imageSource) != nil else { return }
      let thumbnailOptions = [
        String(kCGImageSourceCreateThumbnailFromImageIfAbsent): true,
        String(kCGImageSourceThumbnailMaxPixelSize): 160
        ] as [String : Any]
      if let thumbnailRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, thumbnailOptions as CFDictionary?) {
        thumbnail = NSImage(cgImage: thumbnailRef, size: NSSize.zero)
      } else {
        return nil
      }
    }
  }
}

hello @gmm,

I am a self-taught novice programmer that has done most of my work in Objective C. I wanted to convert some apps I developed to Swift, one which has a CollectionView. My research on doing this brought me to your excellent CollectionView tutorials: Advanced Collection Views in OS X Tutorial and NSCollectionView Tutorial. Like gili I too am able to drag but when dropping nothing happens. Also, I never get the blue line denoting where the drop should take place. I will post the delegate below. From best I can tell the delegate methods for dropping are never called (I setup print statements at the beginning of each method). The results are:

TRACE->ViewController ->NSCollectionViewDelegate-01 in method collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set)
TRACE->ViewController in method highlightItems(_ selected: Bool, atIndexPaths: Set)
<ADAP.CollectionViewItem: 0x610000141d90>{represented object: (null), view: <NSView: 0x610000121fe0> (frame {{267.5, 30}, {160, 140}}), selected: YES}
TRACE->ViewController ->NSCollectionViewDelegate-06 in method collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting?
image file to pasteboard
TRACE->ImageDirectoryLoader: NSObject in method imageFileForIndexPath(_ indexPath: IndexPath) -> ImageFile
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->StickyHeadersLayout: NSCollectionViewFlowLayout in method override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
TRACE->ViewController ->NSCollectionViewDelegate-08 in method collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation)

The version of software I’m using:
XCODE: Version 8.3.2 (8E2002)
MACOS Development Target 10.12
Apple Swift version 3.1 (swiftlang-802.0.53 clang-802.0.42)

I believe my lack of knowledge when converting Swift Structures to Swift 3 are to blame.

This is my first post so if I’m doing something inappropriate let me know. Any help you can provide would be appreciated.

extension ViewController : NSCollectionViewDelegate {

func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
    
    let vExtension = "NSCollectionViewDelegate-01"
    let method = "collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    highlightItems(true, atIndexPaths: indexPaths)
}

func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set<IndexPath>) {
    
    let vExtension = "NSCollectionViewDelegate-02"
    let method = "collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set<IndexPath>) "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    highlightItems(false, atIndexPaths: indexPaths)
}

// 1
private func collectionView(collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAtPoint screenPoint: NSPoint, forItemsAtIndexPaths indexPaths: Set<NSIndexPath>) {
    
    let vExtension = "NSCollectionViewDelegate-03"
    let method = "func collectionView(collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAtPoint screenPoint: NSPoint, forItemsAtIndexPaths indexPaths: Set<NSIndexPath>) "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    indexPathsOfItemsBeingDragged = indexPaths
}

// 2
@nonobjc func collectionView(collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath?>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionViewDropOperation>) -> NSDragOperation {
    
    let vExtension = "NSCollectionViewDelegate-04"
    let method = "func collectionView(collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath?>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionViewDropOperation>) -> NSDragOperation "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    // 3
    if proposedDropOperation.pointee == NSCollectionViewDropOperation.on {
        proposedDropOperation.pointee = NSCollectionViewDropOperation.before
    }
    // 4
    if indexPathsOfItemsBeingDragged == nil {
        return NSDragOperation.copy
    } else {
        return NSDragOperation.move
    }
}
// 1
@nonobjc func collectionView(collectionView: NSCollectionView, canDragItemsAtIndexes indexes: NSIndexSet, withEvent event: NSEvent) -> Bool {
    
    let vExtension = "NSCollectionViewDelegate-05"
    let method = "collectionView(collectionView: NSCollectionView, canDragItemsAtIndexes indexes: NSIndexSet, withEvent event: NSEvent) -> Bool "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    print("returning true from drag")
    return true
}

// 2
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
    
    let vExtension = "NSCollectionViewDelegate-06"
    let method = "collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    print("image file to pasteboard")
    let imageFile = imageDirectoryLoader.imageFileForIndexPath(indexPath as IndexPath)
    return imageFile.url?.absoluteURL as NSPasteboardWriting?

}

// 1
func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionViewDropOperation) -> Bool {
    
    let vExtension = "NSCollectionViewDelegate-07"
    let method = "collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionViewDropOperation) -> Bool "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    if indexPathsOfItemsBeingDragged != nil {
        // 2
        let indexPathOfFirstItemBeingDragged = indexPathsOfItemsBeingDragged.first!
        var toIndexPath: NSIndexPath
        if indexPathOfFirstItemBeingDragged.compare(indexPath as IndexPath) == .orderedAscending {
            toIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        } else {
            toIndexPath = NSIndexPath(forItem: indexPath.item, inSection: indexPath.section)
        }
        // 3
        imageDirectoryLoader.moveImageFromIndexPath(indexPath: indexPathOfFirstItemBeingDragged, toIndexPath: toIndexPath)
        // 4
        collectionView.moveItem(at: indexPathOfFirstItemBeingDragged as IndexPath, to: toIndexPath as IndexPath)
    } else {
        // 5
        var droppedObjects = Array<NSURL>()
        draggingInfo.enumerateDraggingItems(options: NSDraggingItemEnumerationOptions.concurrent, for: collectionView, classes: [NSURL.self], searchOptions: [NSPasteboardURLReadingFileURLsOnlyKey : NSNumber(value: true)]) { (draggingItem, idx, stop) in
            if let url = draggingItem.item as? NSURL {
                droppedObjects.append(url)
            }
        }
        // 6
        insertAtIndexPathFromURLs(urls: droppedObjects, atIndexPath: indexPath as NSIndexPath)
    }
    return true
}

// 7
func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
    
    let vExtension = "NSCollectionViewDelegate-08"
    let method = "collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) "
    print("TRACE->\(module) ->\(vExtension) in method \(method)")

    indexPathsOfItemsBeingDragged = nil
}

In StickyHeadersLayout, I think you should consider sectionInset to caculate the maxY and minY.

CGFloat minY = CGRectGetMinY(attributesForFirstItemInSection.frame) - frame.size.height - self.sectionInset.top;

CGFloat maxY = CGRectGetMaxY(attributesForLastItemInSection.frame) - frame.size.height + self.sectionInset.bottom;

This tutorial is more than six months old so questions are no longer supported at the moment for it. We will update it as soon as possible. Thank you! :]