NSPasteBoard for Drag and Drop - drag-and-drop

I try to move an NSCollectionViewItem within the same NSCollectionView to allow reordering of the items. The collection view item is a custom item representing multiple labels with strings from a custom object Parameter:
class Parameter: NSObject {
var parameterName: String
var parameterNominal: Double
var parameterUpperTolerance: Double
var parameterLowerTolerance: Double
var parameterDistribution: String
var parameterNotes: String
var parameterDataArray: [Double]
// MARK: - Initialization Methods
...
}
Now, do I have to write the whole item/object into the 'NSPasteboard' to use drag and drop within the NSCollectionView correctly?
Most of the drag 'n drop examples works with writing strings to pasteboard using ....register(forDraggedTypes: [NSPasteboardTypeString]).
I currently write one of the items string into the pasteboard and the drag 'n drop starts fine:
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
let pb = NSPasteboardItem()
pb.setString(ParameterList.sharedInstance.parameters[indexPath.item].parameterName, forType: NSPasteboardTypeString)
return pb
}
... but it looks strange to write only part of the object to pasteboard when moving of the complete object is requested, right?
How can I actually write my parameter object to the pasteboard (if needed)?
What is happening behind the scene with the data in the pasteboard when using drag and drop?
Thanks a lot!

I haven't used the pasteboard with collection views, but I have with outline views. If you are just reordering internally dragged items you don't really need to use the paste functionality fully - it is designed to allow generic types to be passed around across apps.
You could just do the equivalent of this:
func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
pasteboardItem.setString("Something to pass - not used", forType:MyConstants.pasteboardType)
return pasteboardItem
}
You'll need to have some way of internally caching the dragged items in the function above. Then, in the accept drop you can just check for your pasteboard type and use your cache.

Related

How can I implement didSelectRowAt method without relying on indexPath

So I'm very new to the Swift programming language and iOS development in general and I'm trying to implement a simple tableview for navigation.
I didn't like the idea of relying on the indexPath parameter to determine which action to perform in my code as changing the order of the cells will need me to go back and refactor the method too. so I was looking at implementing a multi-dimensional array to store my different items for the table.
This works absolutely fine for the cell contents but I'm running into issues when trying to implement the didSelectRowAt method.
Note this is all within a UIViewController class with the UITableViewDataSource delegate.
private let options: [[( title: String, action: (() -> ())? )]] = [
[
("title" , action)
]
]
func action() {
//perform logic here
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let action = self.options[indexPath.section][indexPath.row].action else { return }
action()
}
However I am recieving a build error:
Cannot convert value of type '("MyViewControllerName") -> () -> ()' to expected element type '(() -> ())?'
What am I missing here? Is this even the best way to implement this method?
The issue is that:
1 - action is a method not a simple function.
2 - you are using action before your view controller is actually instantiated (since it's in the init of a stored property).
So there is one simple way to solve your issue:
Replace private let options
by
private lazy var options
for instance.
That way the options will be initialised after your ViewController so the action method will be available as a function.
Overall, it's not a bad idea to try to tie the action to your data.

Best way to tell the UITableView which data should be display in Swift

I'm new to Swift and I followed some tutorials.
They are showing how you are suppose to use a UITableView by using a UITableViewController.
The data displayed in the UITableView are stored in an Array inside the UITableViewController.
I'm OK with it.
Based on this, I tried to make a UITableView with two arrays :
struct Spending {
var title: String
var amount: Float
var date: Date?
}
class TableViewControllerSpending: UITableViewController, SpendingProtocol {
var spendingsTemporary : [Spending] = [Spending(title: "Shoes", amount: 245.99, date: Date())]
var spendingsPermanent : [Spending] = [Spending(title: "Taxes", amount: 125.50, date: Date())]
}
I would like to use 2 arrays to display both of them depending on the navigation. For instance, when you click on a button "My permanent spending" the UITableView only shows the 'permanent' array data or if you click on "All my spending" you can see the content of the 2 arrays.
What is the best solution to do tell the UITableView which data should be display ?
Thank you.
You can try
var isPermanent = true
//
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isPermanent ? spendingsPermanent.count : spendingsTemporary.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = areaSettTable.dequeueReusableCell(withIdentifier:CellIdentifier1) as! notifyTableViewCell
if isPermanent {
}
else {
}
return cell
}
//
Change isPermanent according to the clicked button and then
tableView.reloadData()
Note you can create one array and assign it the current array and deal with only one array
Based on your comment, the best solution is to make TableViewControllerSpending a generic view controller that can render a provided array of Spending objects.
class TableViewControllerSpending: UITableViewController, SpendingProtocol {
var spendings = [Spending]()
}
Implement all of the normal table view methods based on the spendings array.
In some appropriate prepareSegue method called from the two buttons, you get access to the TableViewControllerSpending as the destination controller and then based on the button that was tapped, you set the spendings property with one of the two main lists of Spendings that you have.
With this approach your TableViewControllerSpending has no knowledge that there are two separate lists of data. It just knows how to show a list.

Filtering NSTable while typing into NSTextField - auto-select first row

I have a NSTextView field which filters a NSTable table as user types in the input. I have successfully implemented table filtering.
Now, my goal is to auto-select the first result (the first row in the table) and allow user to use arrow keys to move between the results while typing the search query. When moving between the results in the table, the input field should stay focused. (This is similar to how Spotlight works).
This is how the app looks now:
This is my ViewController:
import Cocoa
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate {
#IBOutlet weak var field: NSTextField!
#IBOutlet weak var table: NSTableView!
var projects: [Project] = []
override func viewDidLoad() {
super.viewDidLoad()
projects = Project.all()
field.delegate = self
table.dataSource = self
table.delegate = self
}
override func controlTextDidChange(_ obj: Notification) {
let query = (obj.object as! NSTextField).stringValue
projects = Project.all().filter { $0.title.contains(query) }
table.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return projects.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "FirstCell"), owner: nil) as? NSTableCellView {
cell.textField?.stringValue = projects[row].title
return cell
}
return nil
}
}
and this is Project class
struct Project {
var title: String = ""
static func all() -> [Project] {
return [
Project(title: "first project"),
Project(title: "second project"),
Project(title: "third project"),
Project(title: "fourth project"),
];
}
}
Thank you
This kinda, sorta has an answer already in the duplicate posted by #Willeke, but 1) that answer is in Objective-C, not Swift, 2) I can provide a somewhat more detailed answer (with pictures!), and 3) I'm brazenly going after the bounty (Rule of Acquisition #110). So, with that in mind, here's how I'd implement what you're trying to do:
Don't use an NSTextView; use an NSTextField, or even better, an NSSearchField. NSSearchField is great because we can set it up in Interface Builder to create the filter predicate with almost no code. All we have to do to do that is to create an NSPredicate property in our view controller, and then set up the search field's Bindings Inspector to point to it:
Then you can create an Array Controller, with its Filter Predicate bound to that same property, and its Content Array binding bound to a property on the view controller:
And, of course, bind the table view to the Array Controller:
Last but not least, bind the text field in your table's cell view to the title property:
With all that set up in Interface Builder, we hardly need any code. All we need is the definition of the Project class (all properties need to be marked #objc so that the Cocoa Bindings system can see them):
class Project: NSObject {
#objc let title: String
init(title: String) {
self.title = title
super.init()
}
}
We also need properties on our view controller for the projects, array controller, and filter predicate. The filter predicate needs to be dynamic so that Cocoa Bindings can be notified when it changes and update the UI. If projects can change, make that dynamic too so that any changes to it will be reflected in the UI (otherwise, you can get rid of dynamic and just make it #objc let).
class ViewController: NSViewController {
#IBOutlet var arrayController: NSArrayController!
#objc dynamic var projects = [
Project(title: "Foo"),
Project(title: "Bar"),
Project(title: "Baz"),
Project(title: "Qux")
]
#objc dynamic var filterPredicate: NSPredicate? = nil
}
And, last but not least, an extension on our view controller conforming it to NSSearchFieldDelegate (or NSTextFieldDelegate if you're using an NSTextField instead of an NSSearchField), on which we'll implement the control(:textView:doCommandBy:) method. Basically we intercept text-editing commands being performed by the search field's field editor, and if we get moveUp: or moveDown:, return true to tell the field editor that we will be handling those commands instead. For everything other than those two selectors, return false to tell the field editor to do what it'd normally do.
Note that this is the reason that you should use an NSTextField or NSSearchField rather than an NSTextView; this delegate method will only be called for NSControl subclasses, which NSTextView is not.
extension ViewController: NSSearchFieldDelegate {
func control(_: NSControl, textView _: NSTextView, doCommandBy selector: Selector) -> Bool {
switch selector {
case #selector(NSResponder.moveUp(_:)):
self.arrayController.selectPrevious(self)
return true
case #selector(NSResponder.moveDown(_:)):
self.arrayController.selectNext(self)
return true
default:
return false
}
}
}
Voilà!
(Of course, if you prefer to populate the table view manually instead of using bindings, you can ignore most of this and just implement control(:textView:doCommandBy:), updating your table's selection manually instead of asking your array controller to do it. Using bindings, of course, results in nice, clean code, which is why I prefer it.)
As #Willeke points out, this is likely a duplicate. The solution from that other question works here. I've converted it to swift and added some explanation.
I tested this with an NSSearchField instead of an NSTextField, but I expect it should work the same.
First, you need to add the NSControlTextEditingDelegate protocol to your ViewController, and add the following function:
func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(moveUp(_:)) {
table.keyDown(with: NSApp.currentEvent!)
return true
} else if commandSelector == #selector(moveDown(_:)) {
table.keyDown(with: NSApp.currentEvent!)
return true
}
return false
}
You've already set the text field's delegate to the ViewController, so you're all set there.
This will cause your NSTextField to first check the delegate before executing the moveUp(_:) selector (triggered by pressing the up arrow). Here, the function responds saying "don't do what you normally do, the delegate will handle it" (by returning true) and sends the event to the NSTableView object instead. Focus is not lost on the text field.

Using swift to populate NSTableView rows with a NSPopupButtonCell

I have been trying to change one of the cells in an NSTableView to a pull-down menu, but have been unsuccessful. I read the Apple developer documentation, but it doesn't give an example of how to use NSPopupButtonCell in a NSTableView. I searched forums, including here, and only found one somewhat relevant example, except that it was in objective-c, so it doesn't work for my swift app. Code for the table is here:
extension DeviceListViewController:NSTableViewDataSource, NSTableViewDelegate{
// get the number of rows for the table
func numberOfRows(in tableView: NSTableView) -> Int {
return homedevices.count
}
// use the data in the homedevices array to populate the table cells
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let result = tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
if tableColumn?.identifier == "ID" {
result.textField?.stringValue = homedevices[row].id
} else if tableColumn?.identifier == "Name" {
result.textField?.stringValue = homedevices[row].name
result.imageView?.image = homedevices[row].image
} else if tableColumn?.identifier == "Type" {
result.textField?.stringValue = homedevices[row].type
} else if tableColumn?.identifier == "Button" {
result.textField?.integerValue = homedevices[row].button
}
return result
}
// facilitates data sorting for the table columns
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
let dataArrayMutable = NSMutableArray(array: homedevices)
dataArrayMutable.sort(using: tableView.sortDescriptors)
homedevices = dataArrayMutable as! [HomeDevice]
tableView.reloadData()
}
}
I really just want to be able to allow pull-down selection to change the button assigned to a particular homedevice (a simple integer), instead of having to type a number into the textfield to edit this value. Unfortuantely, when I add the popupbuttoncell to my table in IB, all of the views for my table cells are removed. So I may need to create the table differently. But most of the things I have read about and tried have caused runtime errors or display an empty table.
EDIT:
Day 3:
Today I have been reading here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingViewTablesWithBindings/PopulatingView-TablesWithBindings.html
and many other places too, but I don't have rep to post any more links.
I have added a NSPopupButton in IB, but am not sure how to set the value. I tried result.objectValue = homedevices[row].button, but that does not work. I suppose that I need an array controller object. So then I tried creating an outlet for the object in my DeviceListViewController like #IBOutlet var buttonArrayController: NSArrayController! I guess that I now need to somehow find a way to connect the array controller to my homedevices array.
so I looked at example code here:
https://github.com/blishen/TableViewPopup
This is in objective-C, which is not a language I am using, but maybe if I keep looking at it at various times over the course of the week, I might figure out how to make a pull-down menu.
So I am continuing to work at this, with no solution currently.
This issue is solved, thanks to #vadian.
The button is inserted as NSPopUpButton object, rather than a NSPopUpButtonCell.
Then the cell gets its own custom class, which I called ButtonCellView as a subclass of NSTableCellView.
Then the created subclass can receive an outlet from the NSPopUpButton to the custom subclass. I can give this a selectedItem variable and create the menu here.
Then in the table view delegate, when making the table, I can just set the selectedItem of my ButtonCellView object to the value from my data array.
It works great!

How to create NSPasteboardWriting for Drag and Drop in NSCollectionView

I have a one-section collection view and would like to implement Drag and Drop to allow reordering of the items. The CollectionViewItem has several textviews showing properties form my Parameter objects. Reading the doc I need to implement the NSCollectionView delegate:
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
let parameter = parameterForIndexPath(indexPath: indexPath)
return parameter // throws an error "Cannot convert return expression of type 'Parameter' to return type 'NSPasteboardWriting?'"
}
I have not found any information understandable for me describing the nature of the NSPasteboardWriting object. So, I have no idea how to proceed...
What is the NSPasteboardWriting object and what do I need to write in the pasteboard?
Thanks!
Disclaimer: I have struggled to find anything out there explaining this in a way that made sense to me, especially for Swift, and have had to piece the following together with a great deal of difficulty. If you know better, please tell me and I will correct it!
The "pasteboardwriter" methods (such as the one in your question) must return something identifiable for the item about to be dragged, that can be written to a pasteboard. The drag and drop methods then pass around this pasteboard item.
Most examples I've seen simply use a string representation of the object. You need this so that in the acceptDrop method you can get your hands back on the originating object (the item being dragged). Then you can re-order that item's position, or whatever action you need to take with it.
Drag and drop involves four principal steps. I'm currently doing this with a sourcelist view, so I will use that example instead of your collection view.
in viewDidLoad() register the sourcelist view to accept dropped objects. Do this by telling it which pasteboard type(s) it should accept.
// Register for the dropped object types we can accept.
sourceList.register(forDraggedTypes: [REORDER_SOURCELIST_PASTEBOARD_TYPE])
Here I'm using a custom type, REORDER_SOURCELIST_PASTEBOARD_TYPE that I define as a constant like so:
`let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item"`
...where the value is something unique to your app ie yourdomain should be changed to something specific to your app eg com.myapp.sourcelist.item.
I define this outside any class (so it can be accessed from several classes) like so:
import Cocoa
let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item"`
class Something {
// ...etc...
implement the view's pasteboardWriterForItem method. This varies slightly depending on the view you're using (i.e. sourcelist, collection view or whatever). For a sourcelist it looks like this:
// Return a pasteboard writer if this outlineview's item should be able to
// drag-and-drop.
func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
let pbItem = NSPasteboardItem()
// See if the item is of the draggable kind. If so, set the pasteboard item.
if let draggableThing = ((item as? NSTreeNode)?.representedObject) as? DraggableThing {
pbItem.setString(draggableThing.uuid, forType: REORDER_SOURCELIST_PASTEBOARD_TYPE)
return pbItem;
}
return nil
}
The most notable part of that is draggableThing.uuid which is simply a string that can uniquely identify the dragged object via its pasteboard.
Figure out if your dragged item(s) can be dropped on the proposed item at the index given, and if so, return the kind of drop that should be.
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
// Get the pasteboard types that this dragging item can offer. If none
// then bail out.
guard let draggingTypes = info.draggingPasteboard().types else {
return []
}
if draggingTypes.contains(REORDER_SOURCELIST_PASTEBOARD_TYPE) {
if index >= 0 && item != nil {
return .move
}
}
return []
}
Process the drop event. Do things such as moving the dragged item(s) to their new position in the data model and reload the view, or move the rows in the view.
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
let pasteboard = info.draggingPasteboard()
let uuid = pasteboard.string(forType: REORDER_SOURCELIST_PASTEBOARD_TYPE)
// Go through each of the tree nodes, to see if this uuid is one of them.
var sourceNode: NSTreeNode?
if let item = item as? NSTreeNode, item.children != nil {
for node in item.children! {
if let collection = node.representedObject as? Collection {
if collection.uuid == uuid {
sourceNode = node
}
}
}
}
if sourceNode == nil {
return false
}
// Reorder the items.
let indexArr: [Int] = [1, index]
let toIndexPath = NSIndexPath(indexes: indexArr, length: 2)
treeController.move(sourceNode!, to: toIndexPath as IndexPath)
return true
}
Aside: The Cocoa mandate that we use pasteboard items for drag and drop seems very unnecessary to me --- why it can't simply pass around the originating (i.e. dragged) object I don't know! Obviously some drags originate outside the application, but for those that originate inside it, surely passing the object around would save all the hoop-jumping with the pasteboard.
The NSPasteboardWriting protocol provides methods that NSPasteboard (well, technically anyone, I guess) can use to generate different representations of an object for transferring around pasteboards, which is an older Apple concept that is used for copy/paste (hence Pasteboard) and, apparently, drag and drop in some cases.
It seems that, basically, a custom implementation of the protocol needs to implement methods that:
tell what UTI types (Apple's way of identifying file types [JPEG, GIF, TXT, DOCX, etc], similar to MIME-types—and that's a fun Google search 😬) your type can be transformed into
writeableTypes(for:) & writingOptions(forType:pasteboard:) to a lesser extent
provide a representation of your class for each of the UTI types you claimed to support
pasteboardPropertyList(forType:)
The other answer provides a straightforward implementation of this protocol for use within a single app.
But practically?
The Cocoa framework classes NSString, NSAttributedString, NSURL, NSColor, NSSound, NSImage, and NSPasteboardItem implement this protocol.
So if you've got a draggable item that can be completely represented as a URL (or String, or Color, or Sound, or Image, etc), just take the URL you have and cast it to NSPasteboardWriting?:
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
let url: URL? = someCodeToGetTheURL(for: indexPath) // or NSURL
return url as NSPasteboardWriting?
}
This is not helpful if you have a complicated type, but I hope it's helpful if you have a collection view of images or some other basic item.