Finder URL interpretation - swift

In this routine - tableView validateDrop, I need to understand the items returned. The return items array appears to be file ids?
func tableView(tableView: NSTableView, validateDrop info: NSDraggingInfo,
proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {
print("validate drop \(dropOperation)")
if dropOperation == .Above {
//get the file URLs from the pasteboard
let pasteboard = info.draggingPasteboard()
//list the file type UTIs we want to accept
let options = [NSPasteboardURLReadingFileURLsOnlyKey : true,
NSPasteboardURLReadingContentsConformToTypesKey : [kUTTypeMovie as String]]
let items = pasteboard.readObjectsForClasses([NSURL.classForCoder()],
options: options)
if items!.count > 0 {
for item in items! {
print("item -> \(item)")
}
print("validate Above -> .Copy")
return .Copy;
} else {
print("validate Above -> .Move")
return .Move
}
}
print("validate other -> .None")
return .None
}
which outputs:
item -> file:///.file/id=6571367.34508463
At some point I need to translate this to what I can use - a typical file:// formatted URL.

duh
po item.filePathURL
▿ Optional<NSURL>
- Some : file:///Users/slashlos/Movies/Flight%20to%20Mars.m4v
which I'd probably unescape for user viewing

Related

(Swift) Strange feedback when drag&drop NSOutlineview

First of all, sorry for my English, I'll try my best to make it clear.(Edited with #Chip Jarred's suggestion,I've made some changes to simplify my question)
What I managed to do is achieving NSOutlineview drag & drop method, using .gap style(I just want to use this style!):
outlineView.draggingDestinationFeedbackStyle = .gap
And problems occurred, it could be easier described by a gif:
https://i.stack.imgur.com/0avhJ.gif
You can see drag&drop can run partly correctly. But the problem is: when I drag a node to the bottom of the list, a bit more below Node4, it will be dragged to the top of the list.
I've tried to fix it so I inserted a "print" fuction in validatDrop{}:
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
print("item:\(item),index:\(index)")
if index < 0 && item == nil{
return []
}else{
outlineView.draggingDestinationFeedbackStyle = .gap
return .move
}
}
The terminal told me that when I dropped a node to the top of the list or the bottom of the list, it returned a same index:
item:nil,index:0
https://i.stack.imgur.com/7pI7s.gif
And if I delete the .gap style:
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
print("item:\(item),index:\(index)")
if index < 0 && item == nil{
return []
}else{
// outlineView.draggingDestinationFeedbackStyle = .gap //ignore this line
return .move
}
}
https://i.stack.imgur.com/a8MMe.gif
Everything became normal. So it could be deduced that it could be not my "move" method's problem.
Sorry for English again, I would be grateful for any help.
And here is the essential part of my code:
outlineView.registerForDraggedTypes([.string])
...
func outlineView(_ outlineView: NSOutlineView,
heightOfRowByItem item: Any) -> CGFloat {
return 15
}
...
extension SceneCatalogView{
func outlineView(_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
let sourceNode = outlineView.row(forItem: item)
return "\(sourceNode)" as NSString
}
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
if index < 0 && item == nil{
return []
}else{
outlineView.draggingDestinationFeedbackStyle = .gap
return .move
}
}
func outlineView(_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool {
let pasteboard = info.draggingPasteboard
let sourceNode = Int(pasteboard.string(forType: .string)!)!
let source = outlineView.item(atRow: sourceNode) as? Catalog
let content = source?.content
let targetNode = outlineView.row(forItem: item)
moveNode(sourceNode, targetNode, index) // Not finished
outlineView.reloadData() // Not finished
return true
}
}
After doing some research, I found multiple reports of a bug with NSTableView when using the .gap dragging style. It would seem that NSOutlineView is inheriting that bug. In any case, I found a work-around.
The problem is that when you drag below the last top-level item, the item and childIndex passed to outlineView(_:acceptDrop:item:childIndex) are always nil and 0, which are exactly the same values you get when dragging to the top of list. The only way I could find to differentiate between the two cases was to use the draggingLocation from NSDraggingInfo to compare against the first item's cell frame, and use that to translate the index.
func translateIndexForGapBug(
_ outlineView: NSOutlineView,
item: Any?,
index: Int,
for info: NSDraggingInfo) -> Int
{
guard outlineView.draggingDestinationFeedbackStyle == .gap,
items.count > 0,
item == nil,
index == 0
else { return index }
let point = outlineView.convert(info.draggingLocation, from: nil)
let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
return outlineView.isFlipped
? (point.y < firstCellFrame.maxY ? index : items.count)
: (point.y >= firstCellFrame.minY ? index : items.count)
}
I call it in outlineView(_:acceptDrop:item:childIndex):
func outlineView(
_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool
{
assert(item == nil || item is Item)
trace("item = \(String(describing: item)), index = \(index)")
guard let sourceTitle = info.draggingPasteboard.string(forType: .string),
let source = parentAndChildIndex(forItemTitled: sourceTitle)
else { return false }
let debuggedIndex = translateIndexForGapBug(
outlineView,
item: item,
index: index,
for: info
)
moveItem(from: source, to: (item as? Item, debuggedIndex))
outlineView.reloadData()
return true
}
Since other drag styles seem to work, I only do this if it's set to .gap, so for the sake of testing, my outlineView(_:validateDrop:proposedItem:proposedChildIndex:) looks like this:
func outlineView(
_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation
{
trace("item = \(String(describing: item)), index = \(index)")
guard info.draggingSource as? NSOutlineView === outlineView else {
return []
}
outlineView.draggingDestinationFeedbackStyle = .gap
if item == nil, index < 0 {
return []
}
return .move
}
However instead of setting it to .gap every time, you could probably just set it once when you set the data source in your outline view.
My definition of Item should be equivalent to your Catalog.
//------------------------------
class Item: CustomStringConvertible
{
var description: String { title }
var title: String
var children: [Item] = []
//------------------------------
init(_ title: String) { self.title = title }
convenience init(_ id: Int) { self.init("Item \(id)") }
//------------------------------
func addChild() {
children.append(Item("\(title).\(children.count + 1)"))
}
//------------------------------
func parentAndChildIndex(forChildTitled title: String) -> (Item?, Int)?
{
for i in children.indices
{
let child = children[i]
if child.title == title { return (self, i) }
if let found = child.parentAndChildIndex(forChildTitled: title){
return found
}
}
return nil
}
}
Here's full implementation of my data source:
//------------------------------
#objc class OVDataSource: NSObject, NSOutlineViewDataSource
{
//------------------------------
// Just creating some items programmatically for testing
var items: [Item] =
{
trace()
let items = (1...4).map { Item($0) }
items[2].addChild()
items[2].addChild()
return items
}()
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any) -> NSPasteboardWriting?
{
trace()
guard let item = item as? Item else { return nil }
return item.title as NSString
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
numberOfChildrenOfItem item: Any?) -> Int
{
trace()
if let item = item {
return (item as? Item)?.children.count ?? 0
}
return items.count
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
child index: Int,
ofItem item: Any?) -> Any
{
trace()
if let item = item as? Item {
return item.children[index]
}
return items[index]
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
isItemExpandable item: Any) -> Bool
{
trace()
if let item = item as? Item {
return item.children.count > 0
}
return false
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation
{
trace("item = \(String(describing: item)), index = \(index)")
guard info.draggingSource as? NSOutlineView === outlineView else {
return []
}
outlineView.draggingDestinationFeedbackStyle = .gap
if item == nil, index < 0 {
return []
}
return .move
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool
{
assert(item == nil || item is Item)
trace("item = \(String(describing: item)), index = \(index)")
guard let sourceTitle = info.draggingPasteboard.string(forType: .string),
let source = parentAndChildIndex(forItemTitled: sourceTitle)
else { return false }
let debuggedIndex = translateIndexForGapBug(
outlineView,
item: item,
index: index,
for: info
)
moveItem(from: source, to: (item as? Item, debuggedIndex))
outlineView.reloadData()
return true
}
//------------------------------
func translateIndexForGapBug(
_ outlineView: NSOutlineView,
item: Any?,
index: Int,
for info: NSDraggingInfo) -> Int
{
guard outlineView.draggingDestinationFeedbackStyle == .gap,
items.count > 0,
item == nil,
index == 0
else { return index }
let point = outlineView.convert(info.draggingLocation, from: nil)
let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
return outlineView.isFlipped
? (point.y < firstCellFrame.maxY ? index : items.count)
: (point.y >= firstCellFrame.minY ? index : items.count)
}
//------------------------------
func parentAndChildIndex(forItemTitled title: String) -> (parent: Item?, index: Int)?
{
trace("Finding parent and child for item: \"\(title)\"")
for i in items.indices
{
let item = items[i]
if item.title == title { return (nil, i) }
if let found = item.parentAndChildIndex(forChildTitled: title) {
return found
}
}
return nil
}
//------------------------------
func moveItem(
from src: (parent: Item?, index: Int),
to dst: (parent: Item?, index: Int))
{
trace("src = \(src), dst = \(dst)")
let item: Item = src.parent?.children[src.index]
?? items[src.index]
if src.parent === dst.parent // Moving item in same level?
{
if let commonParent = src.parent
{
moveItem(
item,
from: src.index,
to: dst.index,
in: &commonParent.children
)
return
}
moveItem(item, from: src.index, to: dst.index, in: &items)
return
}
// Moving between levels
if let srcParent = src.parent {
srcParent.children.remove(at: src.index)
}
else { items.remove(at: src.index) }
if let dstParent = dst.parent {
insertItem(item, into: &dstParent.children, at: dst.index)
}
else { insertItem(item, into: &items, at: dst.index) }
}
//------------------------------
// Move an item within the same level
func moveItem(
_ item: Item,
from srcIndex: Int,
to dstIndex: Int,
in items: inout [Item])
{
if srcIndex < dstIndex
{
insertItem(item, into: &items, at: dstIndex)
items.remove(at: srcIndex)
return
}
items.remove(at: srcIndex)
insertItem(item, into: &items, at: dstIndex)
}
func insertItem(_ item: Item, into items: inout [Item], at index: Int)
{
if index < 0
{
items.append(item)
return
}
items.insert(item, at: index)
}
}
The trace() calls are just for debugging. Either remove them, or implement it:
func trace(
_ message: #autoclosure () -> String = "",
function: StaticString = #function,
line: UInt = #line)
{
#if DEBUG
print("\(function):\(line): \(message())")
#endif
}

How to set the Pasteboard Property List for a custom type - NSPasteboard / Swift

I'm working on a Cocoa application to drag and drop files between two NSTableViews. Rather than using just the URL, I want to use a custom struct so I can have access to more data if needed and not make constant calls to the FileManager.
I believe that I need to implement conform my custom Pasteboard Utility to NSPasteboardReading so I can properly digest the data on the receiving table.
I'm unsure of exactly what's needed to set the init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) function as required when dealing with the custom struct that I'm using in the Pasteboard.
Frankly, I'm unsure of how to use the property list in this situation as I've generally only used it in setting global application plists in the past.
Sadly, there's not a whole lot of resources here. Most examples I've seen generally reference JSON objects for the Property list. I'm unsure if I need to extract the data from the custom Type into an array of Data or String types.
Any guidance here on the implementation or even better guidance on what's possible with Property Lists would be most appreciated!
Custom Struct passed to the PasteBoard:
struct TidiFile {
var url : URL?
var createdDateAttribute : Date?
var modifiedDateAttribute : Date?
var fileSizeAttribute: Int?
//setting for a nil init so this can return nil values in case of failure to set attributes
init( url : URL? = nil,
createdDateAttribute : Date? = nil,
modifiedDateAttribute : Date? = nil,
fileSizeAttribute: Int? = nil) {
self.url = url
self.createdDateAttribute = createdDateAttribute
self.modifiedDateAttribute = modifiedDateAttribute
self.fileSizeAttribute = fileSizeAttribute
}
}
Table View Controller:
Where I write the item to the Pasteboard
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
return PasteboardWriter(tidiFile: tableSourceTidiFileArray[row], at: row)
}
Table View Controller:
Where I want to accept the drop and move the file
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
let pasteboard = info.draggingPasteboard
let pasteboardItems = pasteboard.pasteboardItems
}
Custom Pasteboard Utility:
import Foundation
import Cocoa
class PasteboardWriter: NSObject, NSPasteboardWriting, NSPasteboardReading {
required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
// Need to implement
}
var tidiFile : TidiFile
var index: Int
init(tidiFile : TidiFile, at index: Int) {
self.tidiFile = tidiFile
self.index = index
}
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [.tableViewIndex, .tidiFile]
}
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
switch type {
case .tidiFile:
return tidiFile
case .tableViewIndex:
return index
default:
return nil
}
}
static func readableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [.tableViewIndex, .tidiFile]
}
}
extension NSPasteboard.PasteboardType {
static let tableViewIndex = NSPasteboard.PasteboardType("com.bradzellman.tableViewIndex")
static let tidiFile = NSPasteboard.PasteboardType("com.bradzellman.tidiFile")
}
extension NSPasteboardItem {
open func integer(forType type: NSPasteboard.PasteboardType) -> Int? {
guard let data = data(forType: type) else { return nil }
let plist = try? PropertyListSerialization.propertyList(
from: data,
options: .mutableContainers,
format: nil)
return plist as? Int
}
}
First of all to be able to drag&drop a custom object this object must be a subclass of NSObject.
This is a quick&dirty implementation with non-optional types. The data is serialized to and from Property List with Codable. The protocol methods init(from decoder and encode(to encoder are synthesized.
In init?(pasteboardPropertyList you have to decode an instance and create a new one with the standard initializer.
final class TidiFile : NSObject, Codable {
var url : URL
var createdDateAttribute : Date
var modifiedDateAttribute : Date
var fileSizeAttribute: Int
init(url: URL, createdDateAttribute: Date, modifiedDateAttribute: Date, fileSizeAttribute: Int) {
self.url = url
self.createdDateAttribute = createdDateAttribute
self.modifiedDateAttribute = modifiedDateAttribute
self.fileSizeAttribute = fileSizeAttribute
}
convenience init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
guard let data = propertyList as? Data,
let tidi = try? PropertyListDecoder().decode(TidiFile.self, from: data) else { return nil }
self.init(url: tidi.url, createdDateAttribute: tidi.createdDateAttribute, modifiedDateAttribute: tidi.modifiedDateAttribute, fileSizeAttribute: tidi.fileSizeAttribute)
}
}
extension TidiFile : NSPasteboardWriting, NSPasteboardReading
{
public func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.WritingOptions {
return .promised
}
public func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [.tidiFile]
}
public func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
if type == .tidiFile {
return try? PropertyListEncoder().encode(self)
}
return nil
}
public static func readableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [.tidiFile]
}
public static func readingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.ReadingOptions {
return .asData
}
}

First TableView section title doesn't scroll Swift 4

I'm building the SalesViewControllerfor my app and it consists of a TableView showing all items found in a date range.
Itemis child of Order and it has category, date, itemId, itemName, priceattributes all String.
I finally succeed in displaying the result of itemFetchResultController properly divided in sections as I had wrong sortDescriptor. In configuring itemFetchResultController I want use category property from fetched Item entities to be the section displayed title in populated TableView. My goal is, dough I'm not sure it would be possible or how to achieve it, to only have 1 row per itemName in its section but know ho many of it have been found in fetch and use it to display sold value. It's the first time I use sections and it's all a bit confusing to me. I'm trying to follow Apple documentation sample code gives me a couple of errors in tableView's data source methods as you can see by commented out code. All other posts I found here on stack overflow are very old and in objective c so I don't find answers to my doubts.
So far TableViewgets populated correctly , but first section title doesn't move when scrolling.
Any Idea of what's causing this ?
As always many thanks.
Here is the code I'm using for itemFetchResultController :
lazy var itemFetchedResultController: NSFetchedResultsController<Item> = {
// first sortDescriptor filters the date range: possibly change date from String to dates in both function and CoreData and use "(date >= %#) AND (date <= %#)" instead of "BEGINSWITH" in predicate
let itemFetchRequest = NSFetchRequest<Item>(entityName: "Item")
itemFetchRequest.sortDescriptors = [NSSortDescriptor(key: "category", ascending: true)]
itemFetchRequest.predicate = NSPredicate(format: "order.user.name == %#", UserDetails.fullName!)
itemFetchRequest.predicate = NSPredicate(format: "date BEGINSWITH %#", dateToFetch)
let itemFetchedResultController = NSFetchedResultsController(fetchRequest: itemFetchRequest, managedObjectContext: context, sectionNameKeyPath: "category", cacheName: nil)
return itemFetchedResultController
}()
TableView data source:
func numberOfSections(in tableView: UITableView) -> Int {
// apple doc : trhrows an error : Initializer for conditional binding must have Optional type, not 'NSFetchedResultsController<Item>'
// if let frc = itemFetchedResultController {
// return frc.sections!.count
// }
// return 0
return itemFetchedResultController.sections!.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let sections = self.itemFetchedResultController.sections else {
print(" Error :no sections in fetchedResultController" )
return 0
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "statisticsCell", for: indexPath) as! StatisticsTableViewCell
cell.idInfoLabel.text = itemFetchedResultController.object(at: indexPath).itemId!
cell.nameInfoLabel.text = itemFetchedResultController.object(at: indexPath).itemName!
let item = itemFetchedResultController.object(at: indexPath).itemName!
let productRequest: NSFetchRequest<Product> = Product.fetchRequest()
productRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
productRequest.predicate = NSPredicate(format: "name == %#", item)
productRequest.fetchLimit = 1
do {
let fetch = try context.fetch(productRequest)
cell.productImageView.image = UIImage(data: (fetch[0].productImage! as Data))
cell.minimumStockInfoLabel.text = fetch[0].minimumStock
cell.soldQuantityInfoLabel.text = fetch[0].soldQuantity
} catch {
print("Error in fetching product for cell: \(error)")
}
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// guard let sections = self.itemFetchedResultController.sections else {
// print(" Error : no sections in itemsFetchedResultController " )
// return "empty"
// }
// let sectionInfo = sections[section]
// return sectionInfo.name
guard let sectionInfo = itemFetchedResultController.sections?[section] else {
return nil
}
return sectionInfo.name
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
// if let sectionIndexTitles: FetchedResultController.sectionIndexTitles = self.itemFetchedResultController.sectionIndexTitles {
// print(" Error : no sections in itemsFetchedResultController " )
// return [""]
// }
return itemFetchedResultController.sectionIndexTitles
}
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
// apple doc : trhrows an error : Initializer for conditional binding must have Optional type, not 'Int'
// guard let result = itemFetchedResultController.section(forSectionIndexTitle: title, at: index) else {
// fatalError("Unable to locate section for \(title) at index: \(index)")
// }
// return result
let result = itemFetchedResultController.section(forSectionIndexTitle: title, at: index)
return result
}
Set your UITableView style to grouped and your all section will scroll along with cells

NSBrowser drag and drop swift

I am trying to get drag and drop working in a swift 4 Macos application with a NSBrowser control. The code I have at the moment looks like this:
// Drag and Drop
func browser(_ browser: NSBrowser,
canDragRowsWith rowIndexes: IndexSet,
inColumn column: Int,
with event: NSEvent) -> Bool {
if column != 0 {
quizBrowser.canDragRows(with: rowIndexes, inColumn: column, with: event)
return true
}
return false
}
func browser(_ browser: NSBrowser,
writeRowsWith rowIndexes: IndexSet,
inColumn column: Int,
to pasteboard: NSPasteboard) -> Bool {
if column != 0 {
let row: Int = rowIndexes.last!
let item = quizBrowser.item(atRow: row, inColumn: column) as? BrowserItem
pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: self)
pasteboard.setString(item!.name, forType: NSPasteboard.PasteboardType.string)
return true
}
return false
}
func browser(_ browser: NSBrowser,
validateDrop info: NSDraggingInfo,
proposedRow row: UnsafeMutablePointer<Int>,
column: UnsafeMutablePointer<Int>,
dropOperation: UnsafeMutablePointer<NSBrowser.DropOperation>) -> NSDragOperation {
return NSDragOperation.move
}
func browser(_ browser: NSBrowser,
acceptDrop info: NSDraggingInfo,
atRow row: Int,
column: Int,
dropOperation: NSBrowser.DropOperation) -> Bool {
let pboard = info.draggingPasteboard
let rowData = pboard().data(forType: NSPasteboard.PasteboardType.string)
return true
}
What I am seeing is that the first two functions are being called [canDragRowsWith and writeRowswith], and the setString command is returning true, so it looks as though the value has been saved to the pasteboard.
But the other two functions are not being called. So while I can see the row in the NSBrowser being dragged, it looks as though the control isn't registered to accept the drop. I have also added this in the viewDidLoad function
quizBrowser.setDraggingSourceOperationMask(NSDragOperation.generic, forLocal: true)
Does anyone have an idea what I am missing? Does anyone have a swift 3/4 example of drag and drop using an NSBrowser that they are will to share?
Thanks
Before a view can receive a drag operation, you need to register the data types that it can accept by invoking
func registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType])

swift outline view with two sub children

i working with swift 4 for macOS and i have a NSOutlineView:
i get the data from core data.
structure:
entity Person (relationship to entity Book)
entity Book
My Code for this result:
#IBOutlet weak var myOutlineView: NSOutlineView!
let context = (NSApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var people = [Person]()
override func viewWillAppear() {
requestPeople()
}
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: self) as? CustomCell
if let person = item as? Person {
// Show Person
} else if let book = item as? Book {
// Show Books
}
return view
}
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let person = item as? Person {
return person.books.count
}
return people.count
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let person = item as? Person {
return person.books[index]
}
return people[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let person = item as? Person {
return person.books.count > 0
}
return false
}
func requestPeople() {
let request = NSFetchRequest<Person>(entityName: "Person")
do {
people = try context.fetch(request)
myOutlineView.reloadData()
} catch { print(error) }
}
now my problem:
i would like create another outline view.
My Book entity looks like this (attributes):
name
creationDate
My new outlineview should get this structure:
+ Year
++ Month
+++ Bookname
but i dont know how can I realize this structure.
It is different as my first outline view.
can somebody help me?
=======
i guess that i have create arrays for year and month without duplicates.
for this i try a this function to get the data:
var year = [String]()
var month = [String]()
var books = [Book]()
func requestBooks() {
let request = NSFetchRequest<Book>(entityName: "Book")
do {
books = try context.fetch(request)
for x in 0 ...< books.count {
if !year.contains("\(Calendar.current.component(.year, from: books[x].creationDate))") {
year.append("\(Calendar.current.component(.year, from: books[x].creationDate))")
}
if !month.contains("\(Calendar.current.component(.month, from: books[x].creationDate))") {
month.append("\(Calendar.current.component(.month, from: books[x].creationDate))")
}
}
myOutlineView.reloadData()
} catch { print(error) }
}
A multi-level outline is easier to manage when your underlying data structure is hierarchical (i.e. a tree structure).
Here's an example of how you can create a "Tree" node class for your Books:
class BookNode
{
// levels and relationships, from parent to children
enum Level { case Top, Year, Month, Book }
let subLevels:[Level:Level] = [ .Top:.Year, .Year:.Month, .Month:.Book ]
var label = "" // description and unique "key"
var level = Level.Top
var children : [BookNode] = []
var book : Book! = nil // .Book level will store the actual Book
// add book to hierarchy, auto-create intermediate levels
func add(_ book:Book)
{
var subLabel = ""
switch level
{
case .Top : subLabel = String(Calendar.current.component(.year, from:book.creationDate))
case .Year : subLabel = String(Calendar.current.component(.month, from:book.creationDate))
case .Month : subLabel = book.name
case .Book : self.book = book // last level stores the book
return // and has no children
}
// Add branch (.Year, .Month) or leaf (.Book) node as needed
var subNode:BookNode! = children.first{$0.label == subLabel}
if subNode == nil
{
subNode = BookNode()
subNode.level = subLevels[level]!
subNode.label = subLabel
children.append(subNode)
}
// keep adding recursively down to .Book level
subNode.add(book)
}
}
Your data will be stored in a hierarchy of BookNodes which you can load from your fetch request
(you can pre-sort it, as I did, or leave that up to the BookNode class)
var topNode = BookNode()
func requestBooks()
{
let request = NSFetchRequest<Book>(entityName: "Book")
do {
let books = try context.fetch(request)
topNode = BookNode()
for book in books.sorted(by:{$0.creationDate < $1.creationDate})
{
topNode.add(book)
}
}
}
With this, it will be easy to respond to your outline protocols using the BookNodes as the outline items:
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView?
{
let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), owner: self) as? CustomCell
let node = (item as? BookNode) ?? topNode
switch node.level
{
case .Year : // show year : node.label
case .Month : // show month : node.label
case .Book : // show book name : node.label and/or node.book
default : break
}
return view
}
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int
{
let node = (item as? BookNode) ?? topNode
return node.children.count
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any
{
let node = (item as? BookNode) ?? topNode
return node.children[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool
{
let node = (item as? BookNode) ?? topNode
return node.children.count > 0
}
If you program needs to allow adding/changing/removing individual books, the BookNode class can be used to reflect the individual changed (e.g. remove a book child or add a new one). You will then only need to call reloadData() on the outline without having to get everything back from the database.